⚠

You are viewing archived documentation for pyrith v0.2.0. The latest version is v0.4.0.

Docs/pyrith/Releases/v0.2.0Archived
Framework Reference — v0.2.0

Pyrith

Pure-Python full-stack web framework. HTTP routing, HTML component DSL, full Tailwind CSS engine, design tokens, CVA variants, 30+ UI components, page templates, reactive state, SQLite ORM, session auth, Bearer token auth, CSRF, file uploads, WebSocket rooms, SSE, TestClient — all in Python. Zero external dependencies.

Overview

Pyrith is a monolithic, self-contained web framework for Python 3.9+. The design philosophy is total Python — you never leave the Python ecosystem. No HTML template language, no CSS preprocessor, no JavaScript build step, no configuration file format. Every layer — HTTP server, HTML rendering, CSS generation, database access, session management, form validation — is implemented from scratch using the Python standard library only.

The frontend system rivals React + Tailwind apps in capability: a full Tailwind-equivalent utility system, design tokens, CVA-style variant components, 30+ pre-built UI components, reactive state with live DOM patching over WebSocket, and five production-ready page templates — all without touching JavaScript.

ℹ

Pyrith uses only Python stdlib — no pip install required beyond Pyrith itself.

Installation

pip install pyrith==0.2.0

# with dev extras
pip install "pyrith[dev]==0.2.0"

Requires Python 3.9+. No external runtime dependencies.

⚠

This is an archived release. We recommend upgrading to v0.4.0 (latest) which adds ASGI, OAuth2, MFA, background tasks, payments, AI/LLM, admin panel, telemetry, and 26 new modules — with full backwards compatibility.

Quick Start

from pyrith import Pyrith
from pyrith import ui, style
from pyrith.response import HTMLResponse, JSONResponse

app = Pyrith(__name__)

@app.get('/')
def index(request):
    css = style.CSS()
    css.rule('body', font_family='system-ui', background='#0f172a',
             color='#f1f5f9', min_height='100vh', display='flex',
             align_items='center', justify_content='center')
    css.rule('h1', font_size='3rem', font_weight='800', color='#38bdf8')

    page = ui.Page(title='My Pyrith App')
    page.head_add(f'<style>{css}</style>')
    page.body_add(
        ui.div(
            ui.h1('Hello from Pyrith v0.2.0'),
            ui.p('Pure Python. Zero dependencies. Full stack.'),
        )
    )
    return HTMLResponse(page.to_html())

@app.get('/api/health')
def health(request):
    return JSONResponse({'status': 'ok', 'version': '0.2.0'})

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

Routing

from pyrith import Pyrith
from pyrith.response import JSONResponse

app = Pyrith(__name__)

@app.get('/')
def home(request):
    return '<h1>Home</h1>'

@app.post('/articles')
def create_article(request):
    data = request.json()
    return JSONResponse({'created': True}, status=201)

@app.put('/articles/{article_id}')
def update_article(request):
    return JSONResponse({'id': request.params['article_id']})

@app.route('/profile', methods=['GET', 'POST'])
def profile(request):
    return '<h1>Profile</h1>'

# Route groups
api = app.group('/api/v1')

@api.get('/users')
def list_users(request):
    return JSONResponse({'users': []})

@api.get('/users/{user_id}', name='get_user')
def get_user(request):
    return JSONResponse({'id': request.params['user_id']})

# Reverse URL generation
url = app.url_for('get_user', user_id=42)   # "/api/v1/users/42"

Middleware

from pyrith.middleware import (
    LoggingMiddleware, RequestIDMiddleware, CORSMiddleware,
    SecurityHeadersMiddleware, RateLimitMiddleware, CacheMiddleware,
    StaticFilesMiddleware, GZipMiddleware,
)

app.use(LoggingMiddleware())
app.use(CORSMiddleware(allow_origins=['https://app.example.com'], allow_credentials=True))
app.use(SecurityHeadersMiddleware(hsts=True, frame_options='DENY'))
app.use(RateLimitMiddleware(limit=100, window=60))
app.use(CacheMiddleware(ttl=30, max_size=500))
app.use(StaticFilesMiddleware('./public', '/static'))
app.use(GZipMiddleware(min_size=500))

Request

Method / AttributeReturnsDescription
request.methodstr"GET", "POST", etc.
request.pathstrURL path
request.paramsdictPath parameters
request.querydictQuery string
request.headersdictRequest headers (lowercased keys)
request.json()AnyParse body as JSON
request.form()dictParse form body
request.files()dictUploaded files from multipart
request.bearer_tokenstr | NoneBearer token from Authorization header
request.sessionSessionSession object (requires SessionStore middleware)

Response

from pyrith.response import (
    Response, HTMLResponse, JSONResponse, RedirectResponse,
    StreamResponse, FileResponse, SSEResponse, sse_message,
)

HTMLResponse('<h1>Welcome</h1>')
JSONResponse({'id': 42}, status=201)
RedirectResponse('/login')
StreamResponse(generator(), content_type='text/csv')

resp = JSONResponse({'ok': True})
resp.set_cookie('session', token, max_age=86400, http_only=True, secure=True)
resp.set_header('X-Custom', 'value')

File Uploads

@app.post('/upload/avatar')
def upload_avatar(request):
    file = request.file('avatar')
    if file is None:
        return JSONResponse({'error': 'No file provided'}, status=400)
    if file.content_type not in {'image/jpeg', 'image/png', 'image/webp'}:
        return JSONResponse({'error': 'Unsupported type'}, status=415)
    if file.size > 5 * 1024 * 1024:
        return JSONResponse({'error': 'File too large'}, status=413)
    file.save(f'./uploads/{file.filename}')
    return JSONResponse({'uploaded': file.filename, 'bytes': file.size})

Server-Sent Events

import time
from pyrith.response import SSEResponse, sse_message

@app.get('/events/live-feed')
def live_feed(request):
    def stream():
        for i in range(100):
            yield sse_message(data={'count': i}, event='tick', id=str(i))
            time.sleep(0.5)
    return SSEResponse(stream())

Session Auth

from pyrith.auth import SessionStore
from pyrith.response import JSONResponse, RedirectResponse

store = SessionStore(secret='change-this-in-production', max_age=86400)
app.use(store.middleware())

@app.post('/auth/login')
def login(request):
    data = request.form()
    if authenticate(data['email'], data['password']):
        request.session.set('user_id', 42)
        return RedirectResponse('/dashboard')
    return JSONResponse({'error': 'Invalid credentials'}, status=401)

@app.post('/auth/logout')
def logout(request):
    request.session.clear()
    return RedirectResponse('/login')

Bearer Token Auth

from pyrith.auth import TokenStore, token_required

tokens = TokenStore(secret='change-me-in-production', ttl=3600)

@app.post('/api/auth/token')
def get_token(request):
    data = request.json()
    # Verify credentials ...
    access_token = tokens.create({'user_id': 42, 'role': 'user'})
    return JSONResponse({'access_token': access_token, 'token_type': 'bearer', 'expires_in': 3600})

@app.get('/api/me')
@token_required(tokens)
def get_me(request):
    return JSONResponse({'user_id': request.token_payload['user_id']})

Password Hashing

from pyrith.auth import hash_password, check_password

password_hash = hash_password('correct-horse-battery-staple')
# → "pbkdf2:sha256:260000$<salt>$<key>"

if check_password('correct-horse-battery-staple', stored_hash):
    print('Login successful')

CSRF Protection

from pyrith.auth import CSRFMiddleware

app.use(CSRFMiddleware(secret='csrf-secret-change-me'))
# Every unsafe request (POST, PUT, DELETE, PATCH) must include the token.
# In HTML: <input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">

Route Guards

from pyrith.auth import login_required, role_required, token_required

@app.get('/dashboard')
@login_required(redirect_to='/login')
def dashboard(request):
    return f'<h1>Welcome, user {request.session.get("user_id")}</h1>'

@app.get('/admin/panel')
@login_required()
@role_required('admin', redirect_to='/forbidden')
def admin_panel(request):
    return '<h1>Admin panel</h1>'

ORM

from pyrith.orm import Model, IntField, TextField, BoolField, DateTimeField, ForeignKey

class Post(Model):
    class Meta:
        db = 'app.db'
        table_name = 'posts'

    id         = IntField(primary_key=True)
    title      = TextField(nullable=False)
    slug       = TextField(unique=True)
    published  = BoolField(default=False)
    created_at = DateTimeField()

Post.create_table()

# CRUD
post = Post(title='Hello', slug='hello')
post.save()
posts = Post.objects.filter(published=True).order_by('-created_at').limit(10).all()
Post.objects.filter(slug='hello').update(published=True)
post.delete()

Forms

from pyrith.forms import Form, StringField, EmailField, PasswordField

class LoginForm(Form):
    email    = EmailField(required=True, label='Email')
    password = PasswordField(required=True, min_length=8, label='Password')

@app.post('/login')
def login(request):
    form = LoginForm(request.form())
    if form.is_valid():
        return RedirectResponse('/dashboard')
    return HTMLResponse(form.render(action='/login', method='post'), status=422)

Templates

from pyrith.template import TemplateLoader

loader = TemplateLoader('templates/')

@app.get('/posts')
def post_list(request):
    posts = Post.objects.filter(published=True).all()
    html = loader.render('posts/list.html', posts=posts)
    return HTMLResponse(html)

WebSocket

from pyrith.websocket import WebSocket, rooms

@app.websocket('/ws/chat/{room_name}')
def chat(ws: WebSocket):
    room = rooms.get(ws.request.params.get('room_name', 'global'))
    room.join(ws)
    try:
        while not ws.closed:
            msg = ws.recv()
            if msg is None: break
            room.broadcast(msg, exclude=ws)
    finally:
        room.leave(ws)

HTML DSL

from pyrith import ui

card = ui.div(
    ui.h2('Getting Started'),
    ui.p('Install with pip in under 10 seconds.'),
    ui.code('pip install pyrith'),
    class_='card',
)
print(str(card))

Components

from pyrith.ui import Component
from pyrith import ui
from pyrith.style import cv

button_cv = cv(
    base='inline-flex items-center justify-center rounded-lg font-semibold transition-colors',
    variants={
        'intent': {
            'primary': 'bg-blue-600 text-white hover:bg-blue-700',
            'danger':  'bg-red-600 text-white hover:bg-red-700',
        },
        'size': {'sm': 'h-8 px-3 text-xs', 'md': 'h-10 px-4 text-sm'},
    },
    default_variants={'intent': 'primary', 'size': 'md'},
)

class Button(Component):
    def render(self):
        cls = button_cv(intent=self.props.get('intent', 'primary'), size=self.props.get('size', 'md'))
        return ui.button(self.props.get('label', 'Button'), type_='button', class_=cls)

Component Library

from pyrith.ui import components as c

# Navigation
html = c.navigation.NavBar(brand='MyApp', links=[{'label': 'Docs', 'href': '/docs'}])
html = c.navigation.Tabs(tabs=[{'label':'Overview','value':'overview'}], active='overview')

# Data display
html = c.display.Table(columns=[{'key':'name','label':'Name'}], rows=[{'name':'Alice'}])
html = c.display.Badge('Active', color='green')
html = c.display.Avatar(src='/avatar.jpg', name='Alice Smith', size=48)

# Feedback
html = c.feedback.Alert('Success!', kind='success', title='Done!')
html = c.feedback.Toast('Link copied!', kind='success', duration=3000)
html = c.feedback.Modal(modal_id='delete', title='Confirm', body='Are you sure?')

# Cards & marketing
html = c.cards.HeroSection(title='Build fast', subtitle='With pure Python.')
html = c.cards.PricingCard(name='Pro', price='$29', features=['Unlimited projects'], highlighted=True)

Page Builder

from pyrith.ui import Page
from pyrith.style import ThemeProvider, DarkTheme

provider = ThemeProvider(theme=DarkTheme)
page = Page(title='My App — Dashboard')
page.head_add(provider.to_style_tag())
page.body_add(ui.h1('Dashboard'))
return HTMLResponse(page.to_html())

Icon Library

from pyrith.ui.icons import Icon, icon, list_icons

check_icon = Icon('check', size=20, color='#22c55e')
arrow_icon = Icon('arrow-right', size=16, stroke_width=2.5)
print(list_icons())   # ['arrow-down', 'bell', 'check', 'copy', 'download', ...]

Reactive State

from pyrith.ui.state import Signal, Computed, Effect, useState

count = Signal(0)
count.set(5)
count.update(lambda x: x + 1)

price    = Signal(29.99)
quantity = Signal(3)
total    = Computed(lambda: price.get() * quantity.get())
print(total.get())   # 89.97

value, set_value = useState('initial')

Page Templates

from pyrith.ui.templates import (
    DashboardLayout, AuthLayout, DocsLayout, LandingLayout, BlogLayout,
)

html = DashboardLayout(sidebar='...', header='...', content='...', title='Dashboard')
html = AuthLayout(form='...', brand='MyApp', title='Create account')
html = DocsLayout(sidebar_nav='...', content='...', on_this_page='...')

CSS Engine

from pyrith import style

css = style.CSS()
css.rule('body', font_family='Inter, system-ui, sans-serif', background='#0f172a')
css.rule('a:hover', text_decoration='underline')

mobile = css.media('(max-width: 640px)')
mobile.rule('h1', font_size='2rem')

css.keyframes('fadeIn', {'from': {'opacity': '0'}, 'to': {'opacity': '1'}})
page.head_add(f'<style>{css.to_css()}</style>')

Tailwind Utilities

from pyrith.style import TailwindSheet, tw

sheet = TailwindSheet()
sheet.add('flex', 'items-center', 'gap-4', 'px-6', 'py-4')
sheet.add('w-full', 'md:w-1/2', 'lg:w-1/3')
sheet.add('hover:bg-blue-700', 'dark:bg-zinc-900')
sheet.add('w-[320px]', 'text-[#ff6b6b]')
page.head_add(f'<style>{sheet.to_css()}</style>')

Design Tokens

from pyrith.style.tokens import theme, resolve_color, COLORS

blue500  = theme('colors.blue.500')       # → "#3b82f6"
spacing8 = theme('spacing.8')             # → "2rem"
color    = resolve_color('emerald-400')   # → "#34d399"

Class Variance Authority (CVA)

from pyrith.style import cv

button = cv(
    base='inline-flex items-center justify-center rounded-lg font-semibold',
    variants={
        'intent': {'primary': 'bg-blue-600 text-white', 'ghost': 'hover:bg-gray-100'},
        'size':   {'sm': 'h-8 px-3 text-xs', 'md': 'h-10 px-4 text-sm'},
    },
    default_variants={'intent': 'primary', 'size': 'md'},
)
print(button(intent='ghost', size='sm'))

Theme System

from pyrith.style import Theme, ThemeProvider, LightTheme, DarkTheme

provider = ThemeProvider(theme=LightTheme, dark_theme=DarkTheme)
page.head_add(provider.to_style_tag())

Animations

from pyrith.style import animate, AnimationSheet

hero = ui.h1('Welcome', style=animate('slideInUp', duration='0.6s', easing='cubic-bezier(0.16,1,0.3,1)'))

sheet = AnimationSheet()
sheet.use('fadeIn', 'slideInUp', 'bounce')
page.head_add(f'<style>{sheet.to_css()}</style>')

TestClient

from pyrith import Pyrith, TestClient

app = Pyrith(__name__)

@app.get('/api/health')
def health(request):
    return {'status': 'ok'}

client = TestClient(app)
resp = client.get('/api/health')
assert resp.status_code == 200
assert resp.json()['status'] == 'ok'

CLI

pyrith new myapp   # Scaffold a new project
pyrith run         # Run app.py with debug=True
pyrith version     # Print installed version

You are viewing archived docs for pyrith v0.2.0. Upgrade to v0.4.0 → (adds Virtual DOM, SSG, design tokens, canvas effects, 30+ new components — fully backwards-compatible)