You are viewing archived documentation for pyrith v0.2.0. The latest version is v0.4.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 / Attribute | Returns | Description |
|---|---|---|
| request.method | str | "GET", "POST", etc. |
| request.path | str | URL path |
| request.params | dict | Path parameters |
| request.query | dict | Query string |
| request.headers | dict | Request headers (lowercased keys) |
| request.json() | Any | Parse body as JSON |
| request.form() | dict | Parse form body |
| request.files() | dict | Uploaded files from multipart |
| request.bearer_token | str | None | Bearer token from Authorization header |
| request.session | Session | Session 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 versionYou 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)