Skip to main content

Pyrith

A pure-Python full-stack web framework. Backend routing, HTML components, full Tailwind utility system, design tokens, CVA variants, icon library, reactive state, pre-built component library, page templates, CSS generation, ORM, authentication, file uploads, WebSocket rooms, SSE, CSRF, rate limiting, TestClient — all in Python. Zero external dependencies.

pip install pyrith

Overview

Pyrith is a monolithic, self-contained web framework for Python 3.9+. Everything that a modern web application needs — HTTP server, declarative routing, component-based UI, programmatic CSS, database ORM, session auth, password hashing, Bearer token auth, CSRF protection, file upload handling, multipart parsing, SSE, WebSocket rooms, rate limiting, security headers, response caching, TestClient — lives inside a single package that ships with no dependencies beyond the Python standard library.

The design philosophy is total Python. You never leave the Python ecosystem. There is no HTML template language to learn, no CSS preprocessor to install, no JavaScript build step, no configuration file format, and no external process to run. If you can write Python, you can build a full web application with Pyrith — including production-quality UIs that rival React + Tailwind apps.


Table of Contents

  1. Quick Start
  2. Architecture
  3. HTTP Server & Routing
  4. Middleware
  5. Request Object
  6. Response Objects
  7. File Uploads
  8. Server-Sent Events (SSE)
  9. UI / HTML DSL
  10. Component System
  11. Page Builder
  12. CSS Engine
  13. Full Tailwind Utility System
  14. Design Tokens
  15. Class Variance Authority (CVA)
  16. Animation Utilities
  17. Theme System
  18. Icon Library
  19. Reactive State System
  20. Pre-Built Component Library
  21. Layout Engine
  22. Page Templates
  23. ORM
  24. Template Engine
  25. Session Authentication
  26. Route Guards
  27. Forms & Validation
  28. WebSocket
  29. CLI
  30. Error Handling
  31. PyPI Publishing
  32. Roadmap

Quick Start

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

app = Pyrith(__name__)


@app.get("/")
def index(request):
    css = style.CSS()
    css.rule("body", font_family="system-ui", text_align="center", padding="4rem")
    css.rule("h1", color="#2563eb")

    page = ui.Page(title="Hello Pyrith")
    page.head_add(f"<style>{css}</style>")
    page.body_add(
        ui.h1("Hello from Pyrith"),
        ui.p("A full-stack web framework — 100% Python."),
        ui.a("GitHub →", href="https://github.com/saadsalmanakram/Pyrith"),
    )
    return HTMLResponse(page.to_html())


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8080, debug=True)
python app.py
# [Pyrith] Running on http://127.0.0.1:8080  (debug=True)

Or use the CLI:

pyrith new myapp
cd myapp
python app.py

Architecture

pyrith/
├── app.py           # Pyrith class, HTTP server (ThreadingHTTPServer), middleware pipeline
├── request.py       # Request — method, path, headers, query, body, params, cookies
├── response.py      # Response, HTMLResponse, JSONResponse, RedirectResponse, StreamResponse
├── router.py        # Router, Route (regex param extraction), RouteGroup
├── middleware.py    # Middleware base, CORS, Logging, StaticFiles, GZip
├── websocket.py     # WebSocket frame codec, upgrade handshake (stdlib only)
├── template.py      # Template engine ({{ }}, {% if %}, {% for %}, extends, include)
├── ui/
│   ├── html.py      # Element DSL — div(), span(), h1(), input_(), raw(), …
│   ├── component.py # Component base class, render(), page()
│   ├── page.py      # Page layout builder (head slots, body slots, scripts)
│   ├── state.py     # Signal, Computed, Effect, useState, ReactiveComponent
│   ├── icons.py     # 50+ inline SVG icons (Heroicons paths), Icon(), icon(), list_icons()
│   ├── layout.py    # MasonryGrid, SplitPane, StickyHeader, AspectRatio, ScrollArea
│   ├── components/  # Pre-built UI component library
│   │   ├── layout.py      # Stack, Grid, Container, Spacer, Divider
│   │   ├── navigation.py  # NavBar, Sidebar, Breadcrumb, Tabs, Pagination
│   │   ├── display.py     # Table, Badge, Avatar, Tag, ProgressBar, Spinner, Skeleton, Stat, Timeline
│   │   ├── feedback.py    # Alert, Toast, Modal, Tooltip, Popover
│   │   ├── forms.py       # Input, Textarea, Select, Checkbox, Radio, Switch, FileInput, DatePicker, RangeSlider, SearchInput
│   │   ├── cards.py       # Card, HeroSection, FeatureCard, PricingCard, ProfileCard, EmptyState
│   │   └── interactive.py # Accordion, Dropdown, Collapsible
│   └── templates/   # Full-page layout templates
│       └── layouts.py     # DashboardLayout, AuthLayout, DocsLayout, LandingLayout, BlogLayout
├── style/
│   ├── css.py       # CSS, Rule, MediaQuery — programmatic stylesheet
│   ├── stylesheet.py# StyleSheet — scoped component CSS
│   ├── tailwind.py  # TailwindSheet — full Tailwind utility system + tw() helper
│   ├── tokens.py    # Design tokens — full Tailwind palette, spacing, typography, shadow, etc.
│   ├── variants.py  # ClassVariance (CVA pattern) + cv() factory
│   ├── animations.py# 20+ keyframe animations, AnimationSheet, animate()
│   ├── theme.py     # Theme, ThemeProvider, CSS variables, LightTheme, DarkTheme, PurpleTheme
│   └── units.py     # px(), rem(), em(), pct(), vw(), vh(), fr(), s(), ms()
├── orm/
│   ├── model.py     # Model metaclass, save(), delete(), get(), create_table()
│   ├── fields.py    # IntField, TextField, BoolField, DateTimeField, ForeignKey
│   └── query.py     # QuerySet — filter(), order_by(), limit(), offset(), count()
├── auth/
│   ├── session.py   # SessionStore (HMAC-signed cookies), Session
│   └── decorators.py# login_required(), role_required()
├── forms/
│   └── form.py      # Form, StringField, IntegerField, EmailField, BooleanField
└── cli/
    └── commands.py  # pyrith new / run / version

Key invariant: Every module uses only the Python standard library (http.server, urllib, json, sqlite3, hashlib, hmac, gzip, socket, threading, re, html, mimetypes, time, os). There is intentionally no requirements.txt for the core library.


HTTP Server & Routing

from pyrith import Pyrith

app = Pyrith(__name__)

# Static route
@app.get("/")
def home(request):
    return "<h1>Home</h1>"

# Path parameter
@app.get("/users/{user_id}")
def get_user(request):
    uid = request.params["user_id"]    # always a str
    return f"User: {uid}"

# Multiple methods on one path
@app.route("/items", methods=["GET", "POST"])
def items(request):
    if request.method == "POST":
        return "created"
    return "list"

# Route groups (prefix namespacing)
api = app.group("/api/v1")

@api.get("/status")
def status(request):
    return {"status": "ok"}


# Start the server
app.run(host="0.0.0.0", port=8080, debug=False)

Path parameter syntax

/users/{id}                 → params["id"]
/files/{path}               → params["path"]   (single segment only)
/static/*                   → matches any suffix (wildcard)

Internally, {name} is compiled to the regex capture group ([^/]+). The router iterates routes in declaration order and returns the first match.


Middleware

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

app.use(LoggingMiddleware())
app.use(RequestIDMiddleware())                                    # X-Request-ID on every req/resp
app.use(CORSMiddleware(allow_origins=["https://example.com"]))
app.use(SecurityHeadersMiddleware(                               # CSP, HSTS, X-Frame-Options, ...
    csp="default-src 'self'",
    hsts=True,
    frame_options="DENY",
))
app.use(RateLimitMiddleware(limit=100, window=60))               # 100 req/min per IP
app.use(CacheMiddleware(ttl=30))                                 # 30s in-memory GET cache
app.use(StaticFilesMiddleware(directory="./public", url_prefix="/static"))
app.use(GZipMiddleware(min_size=500))

Built-in middleware

Middleware Description
LoggingMiddleware Logs method, path, status, elapsed ms
RequestIDMiddleware Attaches X-Request-ID UUID to request and response
CORSMiddleware CORS preflight + response headers
SecurityHeadersMiddleware CSP, HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy
RateLimitMiddleware(limit, window) Sliding-window rate limit per IP; returns 429 with Retry-After
CacheMiddleware(ttl, max_size) In-memory GET response cache with LRU eviction
StaticFilesMiddleware Serve directory under URL prefix; path-traversal safe
GZipMiddleware(min_size) Compress responses when client accepts gzip

Writing a custom middleware

from pyrith.middleware import Middleware

class TimingMiddleware(Middleware):
    def before_request(self, request):
        import time
        request.state["start"] = time.perf_counter()
        return None

    def after_request(self, request, response):
        import time
        elapsed = (time.perf_counter() - request.state.get("start", 0)) * 1000
        response.set_header("X-Response-Time", f"{elapsed:.2f}ms")
        return response

app.use(TimingMiddleware())

before_request returns None to continue the pipeline or a Response to short-circuit it. after_request always returns a (possibly modified) Response.


Request Object

Attribute / Method Type Description
request.method str "GET", "POST", etc. (always uppercase)
request.path str URL path, e.g. "/users/42"
request.params dict[str,str] Path parameters from {name} segments
request.query dict[str,Any] Query string key/value pairs
request.headers dict[str,str] All headers, keys lowercased
request.body bytes Raw request body
request.json() Any Parse body as JSON (cached)
request.form() dict Parse application/x-www-form-urlencoded or multipart/form-data
request.files() dict[str, UploadedFile] Uploaded files from multipart/form-data
request.file(name) UploadedFile | None Single uploaded file by field name
request.text() str Decode body as UTF-8 string
request.get_header(name, default) str Case-insensitive header lookup
request.cookies() dict[str,str] Parse Cookie header
request.bearer_token str | None Extract Bearer token from Authorization header
request.content_type str Content-Type header
request.content_length int Content-Length as int
request.is_json bool True if Content-Type is application/json
request.is_form bool True if Content-Type is form/multipart
request.state dict Per-request state dict for middleware communication

Response Objects

from pyrith.response import Response, HTMLResponse, JSONResponse, RedirectResponse, StreamResponse

# Plain text
Response("Hello", status=200)

# HTML
HTMLResponse("<h1>OK</h1>")

# JSON — any dict or list is auto-serialised
JSONResponse({"id": 1, "name": "Alice"}, status=201)

# Redirect
RedirectResponse("/login", status=302)

# Streaming (generator-based)
def gen():
    for chunk in ["one", "two", "three"]:
        yield chunk.encode()
StreamResponse(gen(), content_type="text/plain")

# Cookies
response = JSONResponse({"ok": True})
response.set_cookie("token", "abc", max_age=3600, secure=True, http_only=True)
response.set_header("X-Custom", "value")

Handler return coercion: dict/list → JSONResponse; str starting with < → HTMLResponse; bytes → application/octet-stream; None → 204 No Content; any other str → text/plain.

# File response with ETag + Range support
from pyrith.response import FileResponse
resp = FileResponse("./uploads/report.pdf", as_attachment=True)
resp = resp.serve(request)   # handles If-None-Match (304) and Range (206)
return resp

# Server-Sent Events
from pyrith.response import SSEResponse, sse_message

def event_stream():
    for i in range(5):
        yield sse_message({"count": i}, event="tick")

return SSEResponse(event_stream())

File Uploads

@app.post("/upload")
def upload(request):
    # Automatically handles multipart/form-data
    file = request.file("avatar")
    if file is None:
        return {"error": "No file"}, 400
    file.save(f"./uploads/{file.filename}")
    return {"uploaded": file.filename, "size": file.size}

# Multiple files
@app.post("/gallery")
def gallery(request):
    files = request.files()
    saved = []
    for name, uf in files.items():
        uf.save(f"./gallery/{uf.filename}")
        saved.append(uf.filename)
    return {"saved": saved}

UploadedFile attributes: filename, content_type, data (bytes), size. Method: save(path).


Server-Sent Events (SSE)

from pyrith.response import SSEResponse, sse_message
import time

@app.get("/events")
def events(request):
    def stream():
        for i in range(10):
            yield sse_message({"value": i}, event="update", id=str(i))
            time.sleep(0.5)
    return SSEResponse(stream())

# Format: sse_message(data, event=None, id=None, retry=None)
# data can be a dict (auto-JSON), list, or string

Reverse URL Generation

@app.get("/users/{id}", name="get_user")
def get_user(request):
    return f"User {request.params['id']}"

# Build URL by route name
url = app.url_for("get_user", id=42)      # "/users/42"
url = app.url_for("get_user", id=42, tab="profile")  # "/users/42?tab=profile"

UI / HTML DSL

from pyrith import ui

# Elements are Python calls
card = ui.div(
    ui.h2("Product"),
    ui.p("A great item."),
    ui.a("Buy →", href="/buy"),
    class_="card",
    id="main-card",
)

print(str(card))
# <div class="card" id="main-card"><h2>Product</h2><p>A great item.</p><a href="/buy">Buy →</a></div>

Key conventions

Python syntax HTML output Note
class_="x" class="x" Trailing _ stripped
for_="id" for="id" Same rule
checked=True checked Boolean attribute
disabled=False (omitted) False/None omitted
ui.raw("<b>x</b>") <b>x</b> Bypass HTML escaping

All text content is HTML-escaped automatically. Use ui.raw() only for trusted HTML you construct yourself.

Available elements

div span p h1 h2 h3 h4 h5 h6 a img button input_ textarea form label select option ul ol li table thead tbody tr th td nav header footer main section article aside code pre blockquote strong em br hr script style link meta


Component System

from pyrith.ui import Component
from pyrith import ui

class Alert(Component):
    def render(self):
        kind = self.props.get("kind", "info")
        colors = {"info": "#dbeafe", "warn": "#fef3c7", "error": "#fee2e2"}
        bg = colors.get(kind, colors["info"])
        return ui.div(
            ui.strong(self.props.get("title", "")),
            ui.p(self.props.get("message", "")),
            class_="alert",
            style=f"background:{bg};padding:1rem;border-radius:8px",
        )

alert = Alert(kind="warn", title="Heads up", message="Something needs attention.")
html = str(alert)

Components are plain Python classes. self.props is the kwargs dict passed to the constructor. render() returns any Node (Element, Component, or string).


Page Builder

from pyrith.ui import Page

page = Page(title="My App")
page.head_add('<link rel="stylesheet" href="/static/app.css">')
page.head_add('<meta name="description" content="My Pyrith app">')
page.body_add(ui.nav(...))
page.body_add(ui.main(...))
page.script_add(src="/static/app.js")
page.script_add(inline="console.log('loaded')")

html = page.to_html()

Page builds a complete <!DOCTYPE html> document. Head extras and body nodes are independent lists so you can compose them from multiple places before calling to_html().


CSS Engine

from pyrith import style

css = style.CSS()

# Basic rule
css.rule("body", font_family="system-ui", margin="0", background="#f9fafb")

# Underscores in property names become hyphens: font_size → font-size
css.rule("h1", font_size="2rem", font_weight="700", color="#111827")

# Chained modifier
css.rule("a").set(color="#2563eb", text_decoration="none")

# Media query
mq = css.media("(max-width: 768px)")
mq.rule("h1", font_size="1.5rem")

# Keyframe animation
css.keyframes("fadeIn", {
    "from": {"opacity": "0", "transform": "translateY(8px)"},
    "to":   {"opacity": "1", "transform": "translateY(0)"},
})

# Raw CSS block (for vendor prefixes, complex selectors, etc.)
css.raw(":root { --primary: #2563eb; }")

print(css.to_css())

Scoped component styles

from pyrith.style import StyleSheet

sheet = StyleSheet(scope="card")
sheet.scoped("",       background="#fff", border_radius="12px", padding="1.5rem")
sheet.scoped(" h3",    color="#1d4ed8", margin_top="0")
sheet.scoped(":hover", box_shadow="0 4px 16px rgba(0,0,0,0.12)")
# Emits: .card { … }  .card h3 { … }  .card:hover { … }

Full Tailwind Utility System

Pyrith ships a complete Tailwind CSS equivalent implemented in pure Python. Every utility class compiles to precise CSS declarations. No PostCSS, no Node.js, no build step — just Python.

from pyrith.style import TailwindSheet, tw

# Build a class string for use in HTML attributes
classes = tw("flex", "items-center", "gap-4", "p-6", "bg-gray-900", "text-white",
             "rounded-xl", "shadow-lg", "hover:bg-gray-800", "md:flex-col",
             "dark:bg-zinc-900", "transition-all", "duration-300")

# Generate the actual CSS for those classes
sheet = TailwindSheet()
sheet.add(*classes.split())
print(sheet.to_css())

Supported utility families

Family Examples
Spacing p-4, m-auto, px-6, py-2, mt-8, gap-3, space-x-2
Sizing w-full, h-screen, min-w-0, max-w-7xl, w-1/2, size-10
Typography text-xs → text-9xl, font-bold, leading-6, tracking-wide, uppercase, truncate, line-clamp-3
Colors text-blue-500, bg-red-600, border-green-400, ring-purple-300
Borders border, border-2, border-t, rounded-full, rounded-t-xl, divide-y
Display & Layout hidden, flex, grid, inline-flex, block, contents
Flexbox flex-col, flex-wrap, items-center, justify-between, flex-1, flex-none, grow, shrink
Grid grid-cols-3, col-span-2, col-start-1, row-span-2, auto-cols-fr
Position relative, absolute, fixed, sticky, inset-0, top-4, z-50
Overflow overflow-hidden, overflow-x-auto, overflow-y-scroll
Effects opacity-50, blur-md, brightness-110, contrast-125, saturate-150, grayscale, drop-shadow-lg
Shadows shadow-sm, shadow-lg, shadow-xl, shadow-2xl, shadow-none
Transitions transition-all, transition-colors, duration-300, ease-in-out, delay-150
Transforms scale-110, rotate-45, translate-x-4, translate-y-[-20px], skew-x-6
Animations animate-spin, animate-ping, animate-pulse, animate-bounce
Interactions cursor-pointer, select-none, pointer-events-none, resize, appearance-none
Outline & Ring outline-none, ring-2, ring-offset-2, ring-blue-500
Aspect Ratio aspect-square, aspect-video, aspect-[4/3]
Object Fit object-cover, object-contain, object-fill

Responsive prefixes

All utilities support responsive prefixes: sm:, md:, lg:, xl:, 2xl: — these emit @media (min-width: …) blocks.

sheet.add("w-full", "md:w-1/2", "lg:w-1/3")
# .w-full { width: 100%; }
# @media (min-width: 768px) { .md\:w-1\/2 { width: 50%; } }
# @media (min-width: 1024px) { .lg\:w-1\/3 { width: 33.333%; } }

Dark mode

Use the dark: prefix to emit rules inside @media (prefers-color-scheme: dark):

sheet.add("bg-white", "dark:bg-zinc-900", "text-gray-900", "dark:text-white")

Pseudo-class variants

hover:, focus:, active:, disabled:, checked:, first:, last:, odd:, even:, focus-within:, focus-visible:, group-hover:, placeholder:

sheet.add("hover:scale-105", "focus:ring-2", "disabled:opacity-50", "focus-within:ring-blue-500")

Arbitrary values

Any class with […] is treated as an arbitrary value and emitted verbatim:

sheet.add("w-[320px]", "text-[#ff0000]", "bg-[rgba(0,0,0,0.5)]",
          "top-[calc(100%+8px)]", "grid-cols-[200px_1fr_auto]")

Design Tokens

A complete Tailwind design token system is available as Python dicts:

from pyrith.style.tokens import theme, resolve_color, COLORS, SPACING, FONT_SIZE, SHADOW, BORDER_RADIUS

# Dot-path access to any token
blue500 = theme("colors.blue.500")     # → "#3b82f6"
spacing4 = theme("spacing.4")         # → "1rem"
shadowLg = theme("shadow.lg")         # → "0 10px 15px -3px rgba(0,0,0,0.1) ..."

# Resolve shorthand color names
hex_color = resolve_color("blue-500")  # → "#3b82f6"
white = resolve_color("white")         # → "#ffffff"

Color palette

All 20 Tailwind color families with 11 shades each (50 → 950): slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose, plus white and black.

from pyrith.style.tokens import COLORS
print(COLORS["blue"][500])    # "#3b82f6"
print(COLORS["emerald"][400]) # "#34d399"

Class Variance Authority (CVA)

The CVA pattern (popularized by shadcn/ui) lets you define component variants declaratively — the same pattern used by Radix/shadcn in the React ecosystem, now in Python:

from pyrith.style import cv

# Define a Button with variants
button = cv(
    base="inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none",
    variants={
        "intent": {
            "primary": "bg-blue-600 text-white hover:bg-blue-700",
            "secondary": "bg-gray-100 text-gray-900 hover:bg-gray-200",
            "danger":    "bg-red-600 text-white hover:bg-red-700",
            "ghost":     "hover:bg-gray-100 hover:text-gray-900",
            "link":      "text-blue-600 underline-offset-4 hover:underline",
        },
        "size": {
            "sm": "h-8 px-3 text-xs",
            "md": "h-10 px-4 text-sm",
            "lg": "h-12 px-6 text-base",
            "icon": "h-10 w-10",
        },
    },
    default_variants={"intent": "primary", "size": "md"},
    compound_variants=[
        {"intent": "primary", "size": "lg", "class_": "shadow-lg"},
    ],
)

# Use it
print(button())                           # defaults: primary + md
print(button(intent="danger", size="lg")) # danger + lg + compound shadow-lg
print(button(intent="ghost", class_="mt-4"))  # ghost + extra class

Animation Utilities

from pyrith.style import animate, AnimationSheet, KEYFRAMES

# Inline style string for a single element
style = animate("fadeIn", duration="0.4s", easing="ease-out", delay="0.1s")
# → "animation: fadeIn 0.4s ease-out 0.1s both;"

# Or emit @keyframes CSS for use in a stylesheet
sheet = AnimationSheet()
sheet.use("fadeIn", "slideInUp", "shake", "pulse")
css = sheet.to_css()
# → @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } ...
#   .animate-fadeIn { animation: fadeIn 0.3s ease both; }
#   ...

Available animations: fadeIn, fadeOut, slideInUp, slideInDown, slideInLeft, slideInRight, slideOutUp, slideOutDown, zoomIn, zoomOut, shake, pulse, bounce, spin, ping, float, wiggle, flash, heartbeat, ripple, gradientX, typing, blinkCaret


Theme System

Define a complete design system as Python CSS custom properties:

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

# Custom theme
theme = Theme(
    primary="#2563eb",
    primary_hover="#1d4ed8",
    secondary="#64748b",
    background="#ffffff",
    surface="#f8fafc",
    border="#e2e8f0",
    foreground="#0f172a",
    muted_foreground="#64748b",
    font_sans="Inter, system-ui, sans-serif",
    font_mono="JetBrains Mono, monospace",
    radius="0.5rem",
)

# Emit as CSS variables
print(theme.to_css_vars())
# :root { --color-primary: #2563eb; --color-background: #ffffff; ... }

# Use CSS variable references in your CSS
print(theme.var("primary"))  # → "var(--color-primary)"

# ThemeProvider: auto-switches between light and dark
provider = ThemeProvider(theme=LightTheme, dark_theme=DarkTheme)
html = provider.to_style_tag()
# <style>:root { … } @media (prefers-color-scheme: dark) { :root { … } }</style>

# Embed in a page
page = Page(title="Themed App")
page.head_add(provider.to_style_tag())

Built-in presets: LightTheme, DarkTheme, PurpleTheme


Icon Library

50+ inline SVG icons from the Heroicons set, rendered as Python strings — no external font, no CDN, no base64 blobs:

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

# Render as an HTML string
svg = Icon("check", size=20, color="#22c55e")
svg = Icon("arrow-right", size=16, stroke_width=2.5)
svg = Icon("star", size=24, filled=True)

# Use inside the HTML DSL
from pyrith import ui
el = ui.button(icon("arrow-right"), "Next step", class_="btn")

# Discover all icon names
names = list_icons()  # ['arrow-down', 'arrow-left', 'arrow-right', ...]

Available icons: check, x, arrow-up/down/left/right, chevron-up/down/left/right, menu, search, user, users, home, settings, bell, heart, star, plus, minus, edit, trash, eye, eye-off, download, upload, external-link, link, copy, info, warning, error, success, spinner, loading, calendar, clock, mail, phone, lock, unlock, folder, document, image, chart-bar, shield, code, terminal, cpu, globe, logout, filter, sort, drag, more-horizontal, more-vertical


Reactive State System

Client-side reactivity patterns, implemented server-side in Python. Use Signal/Computed/Effect to build reactive data flows, plus ReactiveComponent for WebSocket-based live DOM patching:

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

# Signal — a reactive value
count = Signal(0)
count.set(1)
count.update(lambda x: x + 1)

# Subscribe to changes
unsubscribe = count.subscribe(lambda val: print(f"count is now {val}"))

# Computed — derives from signals automatically
first = Signal("Alice")
last = Signal("Smith")
full_name = Computed(lambda: f"{first.get()} {last.get()}")
print(full_name.get())   # "Alice Smith"
first.set("Bob")
print(full_name.get())   # "Bob Smith"  ← re-computed automatically

# Effect — runs a side effect when dependencies change
effect(lambda: print(f"Name changed to {full_name.get()}"))

# useState convenience tuple
value, set_value = useState("initial")
set_value("updated")
print(value.get())  # "updated"

ReactiveComponent — live DOM patching over WebSocket

from pyrith.ui.state import Signal, ReactiveComponent

class Counter(ReactiveComponent):
    def __init__(self):
        super().__init__()
        self.count = Signal(0)
        self.track(self.count)   # re-render + push patch when count changes

    def render(self) -> str:
        return f'<div id="counter"><h1>{self.count.get()}</h1></div>'

counter = Counter()

@app.websocket("/live")
def live(ws):
    counter.add_client(ws)
    while not ws.closed:
        msg = ws.recv()
        if msg == "increment":
            counter.count.update(lambda x: x + 1)
            # All subscribed clients receive a JSON patch and update the DOM

Client-side script (ReactiveComponent.PATCH_SCRIPT) is a tiny <script> block that opens the WebSocket and applies JSON patches using element.outerHTML = patch.


Pre-Built Component Library

A complete library of 30+ production-quality components in pyrith.ui.components:

from pyrith.ui import components as c

Layout

c.layout.Stack("<p>A</p>", "<p>B</p>", direction="col", gap="1rem", align="center")
c.layout.Grid("<div>1</div>", "<div>2</div>", "<div>3</div>", cols=3, gap="1.5rem")
c.layout.Container("<p>Content</p>", max_width="800px", center=True)
c.layout.Divider(color="#e5e7eb", thickness="1px", label="OR")

Navigation

c.navigation.NavBar(
    brand="MyApp",
    links=[{"label": "Home", "href": "/"}, {"label": "Docs", "href": "/docs"}],
    actions='<button class="btn">Sign In</button>',
)

c.navigation.Breadcrumb(items=[
    {"label": "Home", "href": "/"},
    {"label": "Blog", "href": "/blog"},
    {"label": "My Post"},  # no href = current page
])

c.navigation.Tabs(
    tabs=[{"label": "Overview", "value": "overview"}, {"label": "API", "value": "api"}],
    active="overview",
    variant="underline",   # "underline" | "pill" | "boxed"
)

c.navigation.Pagination(total=100, page=5, per_page=10)

Display

c.display.Badge("Active", color="green")       # pill badge
c.display.Badge("Beta", color="yellow")

c.display.Avatar(src="/img.jpg", name="Alice Smith", size=48)
c.display.Avatar(name="Alice Smith")           # auto-initials fallback

c.display.ProgressBar(value=72, label="Progress", show_value=True, color="blue")

c.display.Spinner(size=24, color="#3b82f6")

c.display.Skeleton(lines=3, animate=True)      # loading placeholder

c.display.Stat(label="Revenue", value="$42K", delta="12%", delta_type="increase")

c.display.Table(
    columns=[{"key": "name", "label": "Name"}, {"key": "score", "label": "Score"}],
    rows=[{"name": "Alice", "score": "98"}, {"name": "Bob", "score": "87"}],
    striped=True,
    sortable=True,
)

c.display.Timeline(items=[
    {"title": "Signed up", "description": "Welcome!", "date": "Jan 1"},
    {"title": "First purchase", "date": "Jan 15"},
])

Feedback

c.feedback.Alert("Your session will expire in 5 minutes.", kind="warning", title="Warning", dismissible=True)
c.feedback.Alert("Changes saved.", kind="success")

c.feedback.Toast("Copied to clipboard!", kind="success", duration=3000)

# CSS :target modal — no JavaScript required
c.feedback.Modal(
    title="Delete item",
    body="Are you sure you want to delete this? This cannot be undone.",
    modal_id="delete-modal",
    confirm_label="Delete",
    confirm_href="#",
)

c.feedback.Tooltip(text="Hover me", content="Explanation text", placement="top")

c.feedback.Popover(trigger="Click me", content="<p>Popover body</p>")

Forms

c.forms.Input(name="email", type="email", label="Email", placeholder="you@example.com", required=True)
c.forms.Input(name="card", error="Invalid card number")

c.forms.Select(name="role", options=["user", "admin", "moderator"], label="Role")

c.forms.Checkbox(name="agree", label="I agree to the terms", checked=False)
c.forms.Radio(name="plan", options=["free", "pro", "enterprise"], value="pro")
c.forms.Switch(name="notifications", label="Enable email notifications", checked=True)

c.forms.FileInput(name="avatar", label="Profile photo", accept="image/*")
c.forms.DatePicker(name="dob", label="Date of birth", value="2000-01-01")
c.forms.RangeSlider(name="volume", min=0, max=100, value=70, label="Volume")

c.forms.SearchInput(name="q", placeholder="Search…", suggestions=["Python", "Pyrith", "PyPI"])

Cards

c.cards.Card(
    header="<h3>Title</h3>",
    body="<p>Main content goes here.</p>",
    footer='<a href="#">Read more →</a>',
    variant="elevated",   # "solid" | "glass" | "outline" | "elevated"
)

c.cards.HeroSection(
    title="Build beautiful web apps in pure Python",
    subtitle="No JavaScript. No Node. No build step.",
    cta_label="Get Started", cta_href="/docs",
    secondary_label="View on GitHub", secondary_href="https://github.com/...",
)

c.cards.FeatureCard(title="Zero Dependencies", description="Ships with nothing but Python.", icon="⚡", color="blue")

c.cards.PricingCard(
    name="Pro",
    price="$29",
    period="/mo",
    features=["Unlimited projects", "Priority support", "Custom domain"],
    cta_label="Get Pro",
    highlighted=True,   # → "Most Popular" badge + accent border
)

c.cards.ProfileCard(name="Alice Smith", role="Senior Engineer", bio="Loves Python and coffee.", avatar_src="/img.jpg")

c.cards.EmptyState(
    title="No results found",
    description="Try adjusting your filters.",
    icon="🔍",
    action_label="Clear filters", action_href="#",
)

Interactive

c.interactive.Accordion(items=[
    {"title": "What is Pyrith?", "content": "A pure-Python web framework.", "open": True},
    {"title": "Does it need Node?", "content": "No. Zero dependencies."},
])

c.interactive.Dropdown(
    label="Actions",
    items=[
        {"label": "Edit", "href": "/edit"},
        {"label": "Delete", "href": "/delete", "danger": True},
    ],
)

c.interactive.Collapsible(title="Advanced settings", content="<p>Hidden by default.</p>")

Layout Engine

from pyrith.ui.layout import MasonryGrid, SplitPane, StickyHeader, AspectRatio, ScrollArea, ResponsiveLayout

# Pinterest-style masonry
MasonryGrid("<div>Card 1</div>", "<div>Card 2</div>", "<div>Card 3</div>", cols=3, gap="1rem")

# Two-column split
SplitPane(left="<nav>…</nav>", right="<main>…</main>", ratio="240px 1fr", gap="2rem")

# Sticky header with content below
StickyHeader(content="<header>…</header>", offset="0", z_index=100)

# Aspect-ratio box (great for video embeds, images)
AspectRatio("<iframe …></iframe>", ratio="16/9")

# Scrollable area with constrained height
ScrollArea("<p>Lots of content…</p>", max_height="400px")

# Responsive layout: different content per breakpoint
ResponsiveLayout(
    mobile="<div class='mobile-nav'>…</div>",
    tablet="<div class='tablet-nav'>…</div>",
    desktop="<div class='desktop-nav'>…</div>",
)

Page Templates

Five production-ready full-page HTML document builders:

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

# Admin dashboard (sidebar + header + main)
html = DashboardLayout(
    sidebar="<nav>…sidebar navigation…</nav>",
    header="<div>…top bar…</div>",
    content="<div>…page content…</div>",
    title="Admin — MyApp",
)

# Auth page (centered card, gradient background)
html = AuthLayout(
    form="<form>…login form…</form>",
    brand="MyApp",
    title="Sign In",
)

# Three-column docs (sidebar | content | on-this-page TOC)
html = DocsLayout(
    sidebar_nav="<nav>…doc links…</nav>",
    content="<article>…documentation…</article>",
    on_this_page="<ul>…table of contents…</ul>",
    title="Getting Started — MyApp Docs",
)

# Marketing landing page
html = LandingLayout(
    sections=["<section>Hero</section>", "<section>Features</section>"],
    nav="<nav>…brand + links…</nav>",
    footer="<footer>…</footer>",
    title="MyApp — Build Fast",
)

# Blog index
html = BlogLayout(
    posts=[
        {"title": "Hello World", "excerpt": "First post.", "href": "/blog/hello",
         "date": "2026-11-07", "author": "Alice", "tags": ["Python"]},
    ],
    title="My Blog",
)

All templates return a complete <!DOCTYPE html> document with responsive CSS built in. They automatically collapse to single-column on mobile.


ORM

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

class Post(Model):
    class Meta:
        db = "blog.db"           # SQLite file (or ":memory:")
        table_name = "posts"

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


Post.create_table()

# Create
post = Post(title="Hello World", body="First post.", published=True)
post.save()
print(post.id)   # auto-assigned PK

# Read
p = Post.get(title="Hello World")

# QuerySet
drafts = Post.objects.filter(published=False).order_by("-created_at").limit(10).all()
count  = Post.objects.count()

# Update
p.title = "Updated Title"
p.save()

# Delete
p.delete()

# Bulk delete
Post.objects.filter(published=False).delete()

Field types

Field SQLite type Python type
IntField INTEGER int
FloatField REAL float
TextField TEXT str
BoolField INTEGER bool
DateTimeField TEXT datetime.datetime
ForeignKey INTEGER int (raw FK value)

All connections are persistent per-process (connection pool keyed by db path). WAL mode and foreign keys are enabled by default. create_table(if_not_exists=True) is idempotent.

Advanced ORM — filter lookups, bulk ops, transactions

# Django-style filter lookups
Post.objects.filter(score__lt=10).all()
Post.objects.filter(score__gte=100).all()
Post.objects.filter(title__contains="Python").all()
Post.objects.filter(title__startswith="Hello").all()
Post.objects.filter(id__in=[1, 2, 3]).all()
Post.objects.filter(deleted_at__isnull=True).all()

# Exclude
Post.objects.exclude(published=False).all()

# Bulk queryset update
Post.objects.filter(published=False).update(archived=True)

# exists()
Post.objects.filter(slug="my-post").exists()

# Class-method shortcuts
post = Post.create(title="Hello", body="...", published=True)

# Bulk insert (single transaction)
posts = [Post(title=f"Post {i}") for i in range(100)]
Post.bulk_create(posts)

# Explicit transaction
from pyrith.orm import transaction

with transaction(Post):
    post1.save()
    post2.save()
    # Both commit atomically, or both roll back on exception

# Runtime migration (add column to existing table)
Post.add_column("view_count", IntField(default=0))

Template Engine

from pyrith.template import TemplateLoader

loader = TemplateLoader("templates/")

# templates/base.html
# <!DOCTYPE html><html><head><title>{% block title %}App{% endblock %}</title></head>
# <body>{% block content %}{% endblock %}</body></html>

# templates/page.html
# {% extends "base.html" %}
# {% block title %}{{ page_title }}{% endblock %}
# {% block content %}
#   <ul>{% for item in items %}<li>{{ item }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
# {% endblock %}

html = loader.render("page.html", page_title="My List", items=["one", "two", "three"])

Syntax:

  • {{ expr }} — HTML-escaped expression output
  • {{ expr | raw }} — unescaped output
  • {% if expr %}…{% elif expr %}…{% else %}…{% endif %}
  • {% for x in iterable %}…{% empty %}…{% endfor %}
  • {% extends "base.html" %} with {% block name %}…{% endblock %}
  • {% include "partial.html" %}
  • {# comment #} — stripped at render time

Session Authentication

from pyrith.auth import SessionStore, login_required

store = SessionStore(secret="change-this-in-production", max_age=86400)
app.use(store.middleware())   # attaches request.session to every request

@app.post("/login")
def login(request):
    data = request.form()
    if authenticate(data["username"], data["password"]):
        request.session.set("user_id", user.id)
        request.session.set("role", "admin")
        return RedirectResponse("/dashboard")
    return HTMLResponse("Invalid credentials", status=401)

@app.get("/logout")
def logout(request):
    request.session.clear()
    return RedirectResponse("/")

Sessions are stored in memory (dict) and identified by a random 256-bit session ID. The session ID is HMAC-SHA256 signed before being stored in the browser cookie, preventing forgery. The max_age evicts idle sessions server-side.


Password Hashing

from pyrith.auth import hash_password, check_password

# Hash (PBKDF2-SHA256, 260,000 iterations, random salt — OWASP compliant)
hashed = hash_password("user_plaintext_password")
# → "pbkdf2:sha256:260000$<salt>$<hash>"

# Verify (constant-time comparison)
if check_password(submitted_password, stored_hash):
    ...  # valid

Bearer Token Auth

from pyrith.auth import TokenStore, token_required

tokens = TokenStore(secret="change-me", ttl=3600)

@app.post("/auth/token")
def get_token(request):
    data = request.json()
    # validate credentials ...
    token = tokens.create({"user_id": user.id, "role": user.role})
    return {"access_token": token, "token_type": "bearer"}

@app.get("/protected")
@token_required(tokens)
def protected(request):
    user_id = request.token_payload["user_id"]
    return {"user_id": user_id}

# Refresh a near-expiry token
new_token = tokens.refresh(old_token)

Token format: <header_b64>.<payload_b64>.<hmac_sig_b64> — all HMAC-SHA256 signed, no external library.


CSRF Protection

from pyrith.auth import CSRFMiddleware

app.use(CSRFMiddleware(secret="change-me"))
# Every unsafe request (POST/PUT/DELETE) must include the CSRF token.

# In HTML forms:
# <input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">

# In AJAX: send header X-CSRF-Token: <token>
# (token is in the pyrith_csrf cookie, readable by JS since http_only=False)

Route Guards

from pyrith.auth import login_required, role_required

@app.get("/dashboard")
@login_required(redirect_to="/login")
def dashboard(request):
    return "you are logged in"

@app.get("/admin")
@login_required()
@role_required("admin", redirect_to="/forbidden")
def admin(request):
    return "admin panel"

login_required checks request.session.get("user_id") is not None. role_required checks request.session.get("role") == role. Both redirect on failure.


Forms & Validation

from pyrith.forms import (
    Form, StringField, IntegerField, EmailField, BooleanField,
    PasswordField, SelectField, TextareaField, FileField,
)

class RegisterForm(Form):
    username = StringField(required=True, min_length=3, max_length=30)
    email    = EmailField(required=True)
    password = PasswordField(required=True, min_length=8)
    role     = SelectField(choices=["user", "moderator", "admin"], required=True)
    bio      = TextareaField(min_length=0, max_length=500)
    avatar   = FileField(allowed_extensions=["jpg", "png", "gif"], max_size=2*1024*1024)
    agree    = BooleanField(required=True, label="I agree to the terms")

@app.post("/register")
def register(request):
    form = RegisterForm(request.form())
    # Inject uploaded files into form for FileField validation
    for name, uf in request.files().items():
        form._data[name] = uf
    if form.is_valid():
        data = form.cleaned_data
        return RedirectResponse("/welcome")
    return HTMLResponse(form.render(action="/register", method="post"))
Field Description
StringField Text with optional min/max length
IntegerField Integer with optional min/max value
EmailField Email format validation
PasswordField Like StringField, renders as type=password
SelectField(choices) Single-select dropdown; validates against choices
TextareaField Multi-line text with min/max length
FileField File upload; validates extension and size
BooleanField Checkbox

form.errors → dict[str, list[str]]. form.cleaned_data → coerced Python values. form.render() produces a complete <form> with labels, inputs, and inline error messages.


WebSocket

from pyrith.websocket import WebSocket, rooms

@app.websocket("/ws/chat/{room_id}")
def chat(ws: WebSocket):
    room_id = ws._rooms   # set before handler is called — or extract from path
    # Join a named room
    room = rooms.get("global")
    room.join(ws)

    # Notify others
    room.broadcast(f"A new user joined. ({room.size} online)", exclude=ws)

    while not ws.closed:
        msg = ws.recv()
        if msg is None:
            break
        # Echo to sender
        ws.send(f"you: {msg}")
        # Broadcast to all others in the room
        room.broadcast(f"user: {msg}", exclude=ws)

    room.leave(ws)
    room.broadcast(f"A user left. ({room.size} online)")
Feature Detail
RFC 6455 handshake Pure Python, stdlib socket only
Ping/pong keepalive Automatic pong on incoming ping; ws.ping() to send
Thread-safe send ws.send() uses an internal lock
Rooms/broadcast rooms.get(name) → Room; room.broadcast(data, exclude=ws)
Connection cleanup Connections auto-removed from rooms on close

ws.send(str) → text frame; ws.send(bytes) → binary frame; ws.recv() → str | bytes | None; ws.ping() → sends ping; ws.close(code, reason) → graceful close.


Testing

from pyrith import Pyrith, TestClient

app = Pyrith(__name__)

@app.get("/items")
def list_items(request):
    return [{"id": 1}, {"id": 2}]

@app.post("/items")
def create_item(request):
    data = request.json()
    return {"id": 3, **data}

# In tests — no server started, no ports, no sockets
client = TestClient(app)

def test_list_items():
    resp = client.get("/items")
    assert resp.status_code == 200
    assert len(resp.json()) == 2

def test_create_item():
    resp = client.post("/items", json={"name": "Widget"})
    assert resp.status_code == 200
    assert resp.json()["name"] == "Widget"

def test_404():
    resp = client.get("/nonexistent")
    assert resp.status_code == 404

def test_with_query_params():
    resp = client.get("/items", params={"page": "2", "limit": "10"})
    ...

def test_with_form_data():
    resp = client.post("/login", data={"username": "alice", "password": "secret"})
    ...

def test_session_persists():
    client2 = TestClient(app)
    client2.post("/login", data={"username": "alice", "password": "secret"})
    resp = client2.get("/dashboard")
    assert resp.status_code == 200  # session cookie carried automatically

TestClient persists cookies across requests (session, CSRF, etc.), making full end-to-end auth flows testable without a real server.


CLI

pyrith new <project>    Scaffold a new project directory with app.py + requirements.txt
pyrith run [file]       Import and run an app file (default: app.py); calls app.run(debug=True)
pyrith version          Print the installed version
pyrith new blog
cd blog
python app.py

Error Handling

@app.error_handler(404)
def not_found(request):
    return HTMLResponse("<h1>404 — Page not found</h1>", status=404)

@app.error_handler(500)
def server_error(request):
    return HTMLResponse("<h1>500 — Something went wrong</h1>", status=500)

If no handler is registered, the default sends a plain-text response with the status message. In debug=True mode, unhandled exceptions render a styled HTML error page with traceback instead of a bare 500.


Running Tests

pytest tests/ -v

160 tests covering routing, middleware, request/response, multipart file uploads, auth (session, password, tokens, CSRF), ORM (filter lookups, bulk ops, transactions), forms, UI rendering, CSS generation, SSE, TestClient, URL generation, full Tailwind utility system, design tokens, CVA variants, animations, theme system, icons, reactive state, all 30+ UI components, layout engine, and page templates.


PyPI Publishing

The repo ships a GitHub Actions workflow at .github/workflows/publish.yml that uses OIDC trusted publishing (no token stored in secrets):

  1. Go to pypi.org → your project → Publishing → Add a trusted publisher.
  2. Fill in: Owner = your GitHub username, Repo = Pyrith, Workflow = publish.yml, Environment = pypi.
  3. On GitHub, create an environment named pypi (Settings → Environments).
  4. Push your code, then create a GitHub Release — the workflow triggers automatically and publishes the package.

Roadmap

Phase 1 — Core (complete · v0.1.0)

  • HTTP router with path parameters, route groups, reverse URL generation
  • Proper 404/405 handling with Allow header
  • 16 MB body size limit; pretty debug error pages in debug mode
  • Auto-reload dev server (run(reload=True))
  • Middleware pipeline (CORS, Logging, Static Files, GZip)
  • SecurityHeadersMiddleware (CSP, HSTS, X-Frame-Options, etc.)
  • RateLimitMiddleware (sliding-window per IP, 429 + Retry-After)
  • RequestIDMiddleware (UUID4 on every request/response)
  • CacheMiddleware (in-memory GET cache with LRU eviction + X-Cache header)
  • HTML element DSL + Component system + Page builder
  • Programmatic CSS engine + scoped stylesheets + unit helpers
  • ActiveRecord ORM (SQLite backend, QuerySet API)
  • ORM: Django-style filter lookups (__lt, __gte, __in, __contains, __isnull, etc.)
  • ORM: exclude(), update(), exists(), last(), values()
  • ORM: bulk_create(), bulk_update(), create(), add_column()
  • ORM: Explicit transactions, WAL mode, foreign keys enabled
  • Template engine (extends, blocks, for, if, include)
  • Session store (HMAC-signed cookies)
  • Password hashing (PBKDF2-SHA256, 260k iterations, OWASP-compliant)
  • Bearer token auth (HMAC-signed, JWT-compatible, refresh support)
  • CSRF protection (double-submit cookie pattern)
  • Route guards (login_required, role_required, token_required)
  • Form validation — 8 field types including PasswordField, SelectField, TextareaField, FileField
  • Multipart/form-data parsing + file uploads (UploadedFile with save())
  • FileResponse with ETag, If-None-Match (304), Range requests (206)
  • Server-Sent Events (SSEResponse + sse_message helper)
  • WebSocket (RFC 6455, ping/pong keepalive, rooms, broadcast, thread-safe send)
  • TestClient (no network, cookie persistence, full request cycle)
  • CLI (new, run, version)

Phase 2 — Frontend (complete · v0.2.0)

  • Full Tailwind utility class system (all families: spacing, sizing, typography, colors, borders, shadows, effects, layout, flex, grid, position, overflow, transforms, transitions, animations, interactions, ring, outline, aspect-ratio, object-fit)
  • Responsive prefixes: sm:, md:, lg:, xl:, 2xl: → @media (min-width: …)
  • Dark mode: dark: prefix → @media (prefers-color-scheme: dark)
  • Pseudo-class variants: hover:, focus:, active:, disabled:, checked:, first:, last:, group-hover:, focus-within:, focus-visible:, placeholder:
  • Arbitrary value support: w-[320px], text-[#ff0000], bg-[rgba(0,0,0,0.5)]
  • Complete Tailwind design token system (20 color families × 11 shades, spacing, font sizes, shadows, border radius, breakpoints, z-index)
  • Class Variance Authority (CVA) — component variant system matching React/shadcn patterns
  • 23 keyframe animations + AnimationSheet + animate() helper
  • Theme system with CSS custom properties, dark mode media query, built-in presets
  • 50+ inline SVG icons (Heroicons paths), no external font or CDN
  • Reactive state: Signal, Computed, Effect, useState (observer pattern, thread-safe)
  • ReactiveComponent — WebSocket live DOM patching via JSON patches
  • 30+ pre-built UI components: Stack, Grid, Container, NavBar, Sidebar, Breadcrumb, Tabs, Pagination, Table, Badge, Avatar, ProgressBar, Spinner, Skeleton, Stat, Timeline, Alert, Toast, Modal, Tooltip, Popover, Input, Select, Checkbox, Switch, RangeSlider, SearchInput, Card, HeroSection, PricingCard, ProfileCard, EmptyState, Accordion, Dropdown, Collapsible
  • Layout engine: MasonryGrid, SplitPane, StickyHeader, AspectRatio, ScrollArea, ResponsiveLayout
  • 5 full-page HTML templates: DashboardLayout, AuthLayout, DocsLayout, LandingLayout, BlogLayout
  • 160 passing tests

Phase 3 — Ecosystem (planned)

  • PostgreSQL ORM backend (via socket + PostgreSQL wire protocol)
  • Background task queue (threading-based)
  • Declarative API schema + automatic OpenAPI generation
  • Multi-process worker pool
  • Plugin / extension system
  • CLI: pyrith generate component <name> scaffold

License

Apache 2.0 — see LICENSE.


Author

Saad Salman · github.com/saadsalmanakram

Metadata

Release files for pyrith 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyrith 0.3.0
File Size Uploaded Provenance Actions
pyrith-0.3.0.tar.gz 188.2 kB With provenance Details for pyrith-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyrith 0.3.0
File Interpreter ABI Platform Actions
pyrith-0.3.0-py3-none-any.whl Python 3 none any Details for pyrith-0.3.0-py3-none-any.whl

Total release size: 351.0 kB

Release files / pyrith-0.3.0.tar.gz

Download URL pyrith-0.3.0.tar.gz
Size 188.2 kB
Tags Source
SHA-256 checksum
How to use checksums
c464a4f552f3972e33a62ceaf4103df0e64d3cb94649e59661b4585bd2fac592
BLAKE2b-256 checksum
How to use checksums
1cc684b713e0caa1de3ad90f1fad3d2128936f8d9a68b1e82e659458c13d6c21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Release files / pyrith-0.3.0-py3-none-any.whl

Download URL pyrith-0.3.0-py3-none-any.whl
Size 162.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
70ccaee5d353bd59447844f69317a5e6b5e75103ec3ea30670f65d084d88a695
BLAKE2b-256 checksum
How to use checksums
cce1707fb1952f22a611f00f0d780799cdef6c22703450af8a2539a041e538dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Security for pyrith 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pyrith-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page