A content-reactive runtime for Python 3.14t.
import purr
purr.dev("my-site/")Purr unifies the Bengal ecosystem into a single content-reactive runtime. Edit a Markdown file, and the browser updates the affected paragraph — not the page, not the site, just the content that changed. Add a dynamic route alongside your static content without changing frameworks. Deploy as static files or run as a live server. The boundary between static site and web application disappears.
Status: Pre-alpha — Phase 5 (incremental pipeline + observability) complete. Content changes propagate in O(change): incremental re-parse, selective block recompile, and targeted SSE broadcast. Full-stack observability unifies events from Pounce connections, content parsing, template compilation, and browser updates into a single queryable log. See ROADMAP.md for the full plan.
Purr is the integration layer for the Bengal ecosystem. It connects Bengal's dependency graph to Chirp's SSE pipeline, maps Patitas AST changes to Kida template blocks, and unifies static content with dynamic routes. Edit a Markdown file and the browser updates the affected paragraph — not the page, not the site, just the content that changed.
What's good about it:
- Content-reactive — Changes propagate through incremental re-parse, AST diff, selective block recompile, and into the browser via SSE. O(change), not O(document).
- Static-to-dynamic continuum — Start with Markdown and templates. Add Chirp routes when you need search, APIs, or dashboards. Same templates, same server, same URL space.
- Three modes —
purr devfor reactive local development.purr buildfor static export.purr servefor live production with dynamic routes.
# Development server (single worker, reactive pipeline active)
purr dev my-site/
# Static export (renders all routes to HTML files)
purr build my-site/
# Static export with asset fingerprinting and sitemap
purr build my-site/ --base-url https://example.com --fingerprint
# Production server (multi-worker via Pounce)
purr serve my-site/ --workers 4Or programmatically:
import purr
purr.dev("my-site/") # Reactive local development
purr.build("my-site/") # Static export (all routes → HTML files)
purr.serve("my-site/") # Live production serverContent is a reactive data structure, not a build artifact. When you edit a Markdown file, the change propagates through a typed pipeline:
Edit Markdown file
→ Patitas incrementally re-parses only the affected blocks (O(change), not O(document))
→ ASTDiffer identifies which nodes changed (O(1) skip for unchanged subtrees)
→ DependencyGraph resolves affected pages and template blocks
→ Kida selectively recompiles only the changed template blocks (O(changed_blocks))
→ ReactiveMapper maps AST changes to specific block updates
→ Broadcaster pushes HTML fragments via SSE
→ Browser swaps the DOM (htmx, no JS framework)
→ Every step recorded as a typed event in the observability log
This isn't hot-reload. Hot-reload rebuilds the page. Purr traces a content change through the dependency graph to the exact DOM element that needs updating — and every step is observable.
┌─────────────────────────────────────────────────────────┐
│ Browser (htmx SSE subscription on /__purr/events) │
└────────────────────────────┬────────────────────────────┘
│ HTTP + SSE
┌────────────────────────────▼────────────────────────────┐
│ Pounce (ASGI server, free-threading workers) │
└────────────────────────────┬────────────────────────────┘
│ ASGI
┌────────────────────────────▼────────────────────────────┐
│ Purr App │
│ │
│ ContentRouter — Bengal pages as Chirp routes │
│ RouteLoader — user Python routes alongside content │
│ SSE endpoint — /__purr/events │
│ │
│ Reactive Pipeline (dev mode): │
│ FileWatcher → Incremental Parse → ASTDiffer → Mapper │
│ → Block Recompile → SSE Broadcaster │
│ │
│ Observability: │
│ StackCollector ← Pounce events + pipeline events │
│ → EventLog (queryable ring buffer) │
└─────────────────────────────────────────────────────────┘
Add Python routes alongside your static content. Create a file in routes/ — the file
path becomes the URL, and function names map to HTTP methods:
my-site/
├── content/ # Markdown pages (served by Bengal)
├── routes/
│ ├── search.py # GET /search
│ └── api/
│ └── users.py # GET /api/users
└── templates/
# routes/search.py
from chirp import Request, Response
async def get(request: Request) -> Response:
query = request.query.get("q", "")
results = site.search(query)
return request.template("search.html", query=query, results=results)No decorators. No base classes. No registration ceremony. If a function is named get,
post, put, delete, or patch, it handles that HTTP method. If it's named handler,
it handles GET.
Dynamic routes share the same templates and URL space as your content. They appear in
navigation automatically via nav_title, and they access the Bengal site data through
from purr import site.
- Content-reactive. Content is a typed data structure, not a build artifact. Changes propagate through incremental re-parse, AST diff, selective block recompile, and into the browser via SSE — surgically, in O(change), not O(document).
- Static-to-dynamic continuum. Start with Markdown and templates. Add Chirp routes when you need search, APIs, or dashboards. Same templates, same server, same URL space. No migration, no rewrite.
- Three modes.
purr devfor reactive local development.purr buildfor static export to any CDN — renders all routes (content + dynamic), fingerprints assets, and generates a sitemap.purr servefor live production with dynamic routes and real-time updates. - Observable. Every stage of the pipeline — connection, parse, diff, recompile,
broadcast — produces a typed event with nanosecond timestamps. Query the
EventLogby event type, file path, or time range. Full-stack telemetry without logging. - Integration layer. Purr is thin by design — the hard problems are solved by Bengal (content pipeline), Chirp (framework), Kida (templates), Patitas (Markdown), Rosettes (highlighting), and Pounce (server). Purr wires them together.
- Free-threading native. Built on Python 3.14t. Pounce serves with real thread parallelism. Kida compiles templates to Python AST. Patitas parses Markdown with O(n) state machines. No GIL, no fork, no compromise.
- Python >= 3.14
A structured reactive stack — every layer written in pure Python for 3.14t free-threading.
| ᓚᘏᗢ | Bengal | Static site generator | Docs |
| ∿∿ | Purr | Content runtime ← You are here | — |
| ⌁⌁ | Chirp | Web framework | Docs |
| =^..^= | Pounce | ASGI server | Docs |
| )彡 | Kida | Template engine | Docs |
| ฅᨐฅ | Patitas | Markdown parser | Docs |
| ⌾⌾⌾ | Rosettes | Syntax highlighter | Docs |
Python-native. Free-threading ready. No npm required.
MIT