Metadata-Version: 2.4
Name: pyrith
Version: 0.4.0
Summary: A pure-Python universal full-stack web framework — ORM, auth, DB (SQLite/Postgres/MySQL/MongoDB/Redis/D1/Turso), cache, tasks, email, storage, realtime, search, i18n, API, deploy adapters, and glassmorphism UI. Zero required dependencies.
Author-email: Saad Salman <saadsalmanakram1@gmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/saadsalmanakram/Pyrith
Project-URL: Repository, https://github.com/saadsalmanakram/Pyrith
Project-URL: Issues, https://github.com/saadsalmanakram/Pyrith/issues
Keywords: web,framework,backend,frontend,python,fullstack,orm,html,css
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
Provides-Extra: mysql
Requires-Dist: PyMySQL>=1.1; extra == "mysql"
Provides-Extra: mongodb
Requires-Dist: pymongo>=4.0; extra == "mongodb"
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: s3
Requires-Dist: boto3>=1.28; extra == "s3"
Provides-Extra: search
Requires-Dist: sqlite-utils>=3.35; extra == "search"
Provides-Extra: email
Provides-Extra: sentry
Requires-Dist: sentry-sdk>=1.40; extra == "sentry"
Provides-Extra: all
Requires-Dist: psycopg2-binary>=2.9; extra == "all"
Requires-Dist: PyMySQL>=1.1; extra == "all"
Requires-Dist: pymongo>=4.0; extra == "all"
Requires-Dist: redis>=5.0; extra == "all"
Requires-Dist: boto3>=1.28; extra == "all"
Requires-Dist: sentry-sdk>=1.40; extra == "all"
Dynamic: license-file

# 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](#quick-start)
2. [Architecture](#architecture)
3. [HTTP Server & Routing](#http-server--routing)
4. [Middleware](#middleware)
5. [Request Object](#request-object)
6. [Response Objects](#response-objects)
7. [File Uploads](#file-uploads)
8. [Server-Sent Events (SSE)](#server-sent-events-sse)
9. [UI / HTML DSL](#ui--html-dsl)
10. [Component System](#component-system)
11. [Page Builder](#page-builder)
12. [CSS Engine](#css-engine)
13. [Full Tailwind Utility System](#full-tailwind-utility-system)
14. [Design Tokens](#design-tokens)
15. [Class Variance Authority (CVA)](#class-variance-authority-cva)
16. [Animation Utilities](#animation-utilities)
17. [Theme System](#theme-system)
18. [Icon Library](#icon-library)
19. [Reactive State System](#reactive-state-system)
20. [Pre-Built Component Library](#pre-built-component-library)
21. [Layout Engine](#layout-engine)
22. [Page Templates](#page-templates)
23. [ORM](#orm)
24. [Template Engine](#template-engine)
25. [Session Authentication](#session-authentication)
26. [Route Guards](#route-guards)
27. [Forms & Validation](#forms--validation)
28. [WebSocket](#websocket)
29. [CLI](#cli)
30. [Error Handling](#error-handling)
31. [PyPI Publishing](#pypi-publishing)
32. [Roadmap](#roadmap)

---

## Quick Start

```python
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

```python
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

```python
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

```python
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

```python
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`.

```python
# 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

```python
@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)

```python
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

```python
@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

```python
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

```python
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

```python
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

```python
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

```python
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.

```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.

```python
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)`:

```python
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:`

```python
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:

```python
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:

```python
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`.

```python
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:

```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

```python
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:

```python
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:

```python
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:

```python
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

```python
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`:

```python
from pyrith.ui import components as c
```

### Layout

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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:

```python
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

```python
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

```python
# 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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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
```

```bash
pyrith new blog
cd blog
python app.py
```

---

## Error Handling

```python
@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

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

### Phase 2 — Frontend (complete · v0.2.0)
- [x] 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)
- [x] Responsive prefixes: `sm:`, `md:`, `lg:`, `xl:`, `2xl:` → `@media (min-width: …)`
- [x] Dark mode: `dark:` prefix → `@media (prefers-color-scheme: dark)`
- [x] Pseudo-class variants: `hover:`, `focus:`, `active:`, `disabled:`, `checked:`, `first:`, `last:`, `group-hover:`, `focus-within:`, `focus-visible:`, `placeholder:`
- [x] Arbitrary value support: `w-[320px]`, `text-[#ff0000]`, `bg-[rgba(0,0,0,0.5)]`
- [x] Complete Tailwind design token system (20 color families × 11 shades, spacing, font sizes, shadows, border radius, breakpoints, z-index)
- [x] Class Variance Authority (CVA) — component variant system matching React/shadcn patterns
- [x] 23 keyframe animations + AnimationSheet + `animate()` helper
- [x] Theme system with CSS custom properties, dark mode media query, built-in presets
- [x] 50+ inline SVG icons (Heroicons paths), no external font or CDN
- [x] Reactive state: Signal, Computed, Effect, useState (observer pattern, thread-safe)
- [x] ReactiveComponent — WebSocket live DOM patching via JSON patches
- [x] 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
- [x] Layout engine: MasonryGrid, SplitPane, StickyHeader, AspectRatio, ScrollArea, ResponsiveLayout
- [x] 5 full-page HTML templates: DashboardLayout, AuthLayout, DocsLayout, LandingLayout, BlogLayout
- [x] 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](LICENSE).

---

## Author

**Saad Salman** · [github.com/saadsalmanakram](https://github.com/saadsalmanakram)
