{"id":44150452,"url":"https://github.com/lbliii/chirp","last_synced_at":"2026-04-23T16:00:53.226Z","repository":{"id":337089739,"uuid":"1152291210","full_name":"lbliii/chirp","owner":"lbliii","description":"⌁⌁ Chirp — HTML-first web framework for Python 3.14+ with streaming, SSE, and fragment rendering","archived":false,"fork":false,"pushed_at":"2026-04-17T13:19:48.000Z","size":3102,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-17T15:25:05.374Z","etag":null,"topics":["asgi","free-threading","html-over-the-wire","htmx","hypermedia","no-build","python","server-sent-events","sse","streaming","web-framework"],"latest_commit_sha":null,"homepage":"https://lbliii.github.io/chirp/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lbliii.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-07T16:56:19.000Z","updated_at":"2026-04-17T13:20:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lbliii/chirp","commit_stats":null,"previous_names":["lbliii/chirp"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/lbliii/chirp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lbliii%2Fchirp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lbliii%2Fchirp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lbliii%2Fchirp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lbliii%2Fchirp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lbliii","download_url":"https://codeload.github.com/lbliii/chirp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lbliii%2Fchirp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32187404,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-23T15:28:30.493Z","status":"ssl_error","status_checked_at":"2026-04-23T15:28:29.972Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asgi","free-threading","html-over-the-wire","htmx","hypermedia","no-build","python","server-sent-events","sse","streaming","web-framework"],"created_at":"2026-02-09T03:15:30.104Z","updated_at":"2026-04-23T16:00:53.218Z","avatar_url":"https://github.com/lbliii.png","language":"Python","funding_links":[],"categories":["Micro-frameworks"],"sub_categories":["Async"],"readme":"# ⌁⌁ Chirp\n\n[![PyPI version](https://img.shields.io/pypi/v/bengal-chirp.svg)](https://pypi.org/project/bengal-chirp/)\n[![Python 3.14+](https://img.shields.io/badge/python-3.14+-blue.svg)](https://pypi.org/project/bengal-chirp/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://pypi.org/project/bengal-chirp/)\n\n**A Python web framework for HTMX, HTML fragments, streaming HTML, and Server-Sent Events.**\n\n```python\nfrom chirp import App\n\napp = App()\n\n@app.route(\"/\")\ndef index():\n    return \"Hello, World!\"\n\napp.run()\n```\n\n---\n\n## What is Chirp?\n\nChirp is a Python web framework built for the modern web platform: browser-native UI, HTML over the wire, streaming responses, and Server-Sent Events. Routes return intent — `Page`, `Fragment`, `OOB`, `EventStream`, `Suspense` — and the framework handles content negotiation, layout composition, and htmx awareness automatically. One template with named blocks serves as a full page, a fragment endpoint, an SSE payload, and a Suspense deferred block. No `make_response()`. No `jsonify()`. The type *is* the intent.\n\n```python\n@app.route(\"/search\")\nasync def search(request: Request):\n    results = await db.search(request.query.get(\"q\", \"\"))\n    return Page(\"search.html\", \"results\", results=results)\n    # Full page for browsers. Fragment for htmx. Same template, same data.\n```\n\n- **Browser-native UI** — `\u003cdialog\u003e`, `popover`, View Transitions, container queries. Let the browser be the framework.\n- **HTML over the wire** — Full pages, fragments, streaming HTML, and SSE. Built for htmx.\n- **Streaming HTML** — Shell first, content fills in as data arrives. No loading spinners.\n- **Server-Sent Events** — Real-time updates over plain HTTP. No WebSocket upgrade required.\n- **MCP tools** — Register functions as tools callable by LLMs and MCP clients.\n\nRead the [Philosophy](docs/philosophy.md) for the full picture.\n\n## Use Chirp For\n\n- **HTMX-driven web apps** — Server-rendered UI with fragment swaps and progressive enhancement\n- **Server-rendered applications** — Full pages plus partial updates from the same templates\n- **Streaming interfaces** — Progressive HTML delivery and token-by-token responses\n- **Real-time dashboards** — SSE-powered updates without WebSocket complexity\n- **Teams avoiding heavy frontend stacks** — HTML, CSS, templates, and browser-native features\n\n---\n\n## Installation\n\n```bash\n# pip\npip install bengal-chirp\n\n# uv\nuv add bengal-chirp\n```\n\nRequires Python 3.14+.\n\nChirp works on its own with plain templates. `chirp-ui` is an optional companion UI layer, not part of the framework core.\n\n---\n\n## Quick Start\n\n```bash\nchirp new myapp \u0026\u0026 cd myapp \u0026\u0026 python app.py\n```\n\n| Function | Description |\n|----------|-------------|\n| `chirp new \u003cname\u003e` | Scaffold an auth-ready project |\n| `chirp new \u003cname\u003e --shell` | Scaffold with a persistent app shell (topbar + sidebar) |\n| `chirp new \u003cname\u003e --sse` | Scaffold with SSE boilerplate (`EventStream`, `sse_scope`) |\n| `chirp run \u003capp\u003e` | Start the dev server from an import string |\n| `chirp check \u003capp\u003e` | Validate hypermedia contracts |\n| `chirp check \u003capp\u003e --warnings-as-errors` | Fail CI on contract warnings |\n| `chirp routes \u003capp\u003e` | Print the registered route table |\n| `App()` | Create an application |\n| `@app.route(path)` | Register a route handler |\n| `Template(name, **ctx)` | Render a full template |\n| `Template.inline(src, **ctx)` | Render from string (prototyping) |\n| `Page(name, block, **ctx)` | Auto Fragment or Template based on request |\n| `PageComposition(template, fragment_block, ...)` | Python-first composition with regions |\n| `Fragment(name, block, **ctx)` | Render a named template block |\n| `Stream(name, **ctx)` | Stream HTML progressively |\n| `Suspense(name, **ctx)` | Shell first, OOB swaps for deferred data |\n| `EventStream(gen)` | Server-Sent Events stream |\n| `hx_redirect(url)` | Redirect helper for htmx and full-page requests |\n| `app.run()` | Start the development server |\n\n---\n\n## Streaming: `Stream` vs `Suspense` vs `EventStream`\n\nPicking the wrong one is the most common return-type mistake. Use this table:\n\n| Type | Shell first? | Transport | Use for | Not for |\n|------|--------------|-----------|---------|---------|\n| `Stream` | No — flush blocks as they complete | Single chunked HTTP response | Slow first-byte pages with independent sections (SEO-friendly progressive render) | Post-load updates |\n| `Suspense` | Yes — shell renders, deferred blocks stream as OOB swaps | Single chunked HTTP response | Dashboards / detail pages with multiple slow data sources, one round trip | Post-load updates |\n| `EventStream` | N/A — pure event channel | SSE (`text/event-stream`, long-lived) | Notifications, tickers, chat tails *after* the page loads | Initial page render |\n\n**Rule of thumb**: initial render that streams → `Suspense` (or `Stream` for SEO-heavy sections); updates after the page loads → `EventStream`. If you're hesitating between `Suspense` and `EventStream`, ask: *is this the initial render or a post-load update?*\n\n---\n\n## Features\n\n| Feature | Description | Docs |\n|---------|-------------|------|\n| **HTMX Patterns** | Search, inline edit, infinite scroll, modal, and fragment workflows | [htmx Patterns →](https://lbliii.github.io/chirp/docs/tutorials/htmx-patterns/) |\n| **Comparison** | When Chirp fits compared with Flask, FastAPI, and Django | [When to Use Chirp →](https://lbliii.github.io/chirp/docs/about/comparison/) |\n| **Routing** | Pattern matching, path params, method dispatch | [Routing →](https://lbliii.github.io/chirp/docs/routing/) |\n| **Filesystem routing** | Route discovery from `pages/` with layouts | [Filesystem →](https://lbliii.github.io/chirp/docs/routing/filesystem-routing/) |\n| **Route directory contract** | `_meta.py`, `_context.py`, `_actions.py`, sections, shell context, and route validation | [Route Directory →](https://lbliii.github.io/chirp/docs/guides/route-directory/) |\n| **Route introspection** | Reserved files, inheritance rules, debug headers, and route explorer | [Route Contract →](https://lbliii.github.io/chirp/docs/reference/route-contract/) |\n| **Templates** | Kida integration, rendering, filters | [Templates →](https://lbliii.github.io/chirp/docs/templates/) |\n| **Fragments** | Render named template blocks independently | [Fragments →](https://lbliii.github.io/chirp/docs/templates/fragments/) |\n| **Forms** | `form_or_errors`, form macros, validation | [Forms →](https://lbliii.github.io/chirp/docs/data/forms-validation/) |\n| **Validation** | `chirp.validation` — composable rules (`required`, `email`, `max_length`, …) returning a `ValidationResult` | [Forms →](https://lbliii.github.io/chirp/docs/data/forms-validation/) |\n| **Streaming** | Progressive HTML rendering via Kida | [Streaming →](https://lbliii.github.io/chirp/docs/streaming/) |\n| **SSE** | Server-Sent Events for real-time updates | [SSE →](https://lbliii.github.io/chirp/docs/streaming/server-sent-events/) |\n| **Middleware** | CORS, sessions, static files, security headers, custom | [Middleware →](https://lbliii.github.io/chirp/docs/middleware/) |\n| **Contracts** | Validate htmx attrs, form actions, and route-bearing dialog args | [Reference →](https://lbliii.github.io/chirp/docs/reference/) |\n| **Testing** | Test client, assertions, isolation utilities | [Testing →](https://lbliii.github.io/chirp/docs/testing/) |\n| **Data** | Database integration and form validation | [Data →](https://lbliii.github.io/chirp/docs/data/) |\n| **Optional UI layer** | `chirp-ui` companion components and styles | [chirp-ui →](https://github.com/lbliii/chirp-ui) |\n\n📚 **Full documentation**: [lbliii.github.io/chirp](https://lbliii.github.io/chirp/)\n\n---\n\n## Benchmarks\n\nChirp now ships a synthetic benchmark suite for comparing Chirp, FastAPI, and Flask across JSON and CPU workloads, plus Chirp-specific fused sync and mixed JSON+SSE scenarios.\n\n```bash\nuv sync --extra benchmark\nuv run poe benchmark\n```\n\nSee [`benchmarks/README.md`](benchmarks/README.md) for how the benchmarks work, their caveats, and the available runners.\n\n---\n\n## Production Deployment\n\nChirp apps run on **[Pounce](https://github.com/lbliii/pounce)**, a production-grade ASGI server with HTTP/2, graceful shutdown, Prometheus metrics, rate limiting, and multi-worker scaling. See the [deployment guide](https://lbliii.github.io/chirp/docs/deployment/production/) for details.\n\n---\n\n## Usage\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eReturn Values\u003c/strong\u003e — Type-driven content negotiation\u003c/summary\u003e\n\nRoute functions return *values*. The framework handles content negotiation based on the type:\n\n```python\nreturn \"Hello\"                                  # -\u003e 200, text/html\nreturn {\"users\": [...]}                         # -\u003e 200, application/json\nreturn Template(\"page.html\", title=\"Home\")      # -\u003e 200, rendered via Kida\nreturn Page(\"search.html\", \"results\", items=x)  # -\u003e Fragment or Template (auto)\nreturn Fragment(\"page.html\", \"results\", items=x) # -\u003e 200, rendered block\nreturn Stream(\"dashboard.html\", **async_ctx)    # -\u003e 200, streamed HTML\nreturn Suspense(\"dashboard.html\", stats=...)    # -\u003e shell + OOB swaps\nreturn EventStream(generator())                 # -\u003e SSE stream\nreturn hx_redirect(\"/dashboard\")                # -\u003e Location + HX-Redirect\nreturn Response(body=b\"...\", status=201)         # -\u003e explicit control\nreturn Redirect(\"/login\")                       # -\u003e 302\n```\n\nNo `make_response()`. No `jsonify()`. The type *is* the intent.\n\nFor htmx-driven form posts or mutations that should trigger a full-page\nnavigation, prefer `hx_redirect()` so both plain browser and htmx requests\nfollow the redirect correctly.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eFragments and htmx\u003c/strong\u003e — Render template blocks independently\u003c/summary\u003e\n\nKida can render a named block from a template independently, without rendering the whole page:\n\n```html\n{# templates/search.html #}\n{% extends \"base.html\" %}\n\n{% block content %}\n  \u003cinput type=\"search\" hx-get=\"/search\" hx-target=\"#results\" name=\"q\"\u003e\n  {% block results_list %}\n    \u003cdiv id=\"results\"\u003e\n      {% for item in results %}\n        \u003cdiv class=\"result\"\u003e{{ item.title }}\u003c/div\u003e\n      {% end %}\n    \u003c/div\u003e\n  {% endblock %}\n{% endblock %}\n```\n\n```python\n@app.route(\"/search\")\nasync def search(request: Request):\n    results = await db.search(request.query.get(\"q\", \"\"))\n    if request.is_fragment:\n        return Fragment(\"search.html\", \"results_list\", results=results)\n    return Template(\"search.html\", results=results)\n```\n\nFull page request renders everything. htmx request renders just the `results_list` block.\nSame template, same data, different scope. No separate \"partials\" directory.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eForms and validation\u003c/strong\u003e — \u003ccode\u003echirp.validation\u003c/code\u003e + \u003ccode\u003eValidationError\u003c/code\u003e\u003c/summary\u003e\n\n`chirp.validation` is a small, composable rule library. Validators are plain\ncallables (`(str) -\u003e str | None`); rules compose into a dict; `validate()`\nreturns a `ValidationResult` that's truthy iff the form is clean.\n\n```python\nfrom chirp import Page, ValidationError\nfrom chirp.validation import validate, required, email, max_length\n\n@app.route(\"/contacts\", methods=[\"POST\"])\nasync def create_contact(request: Request):\n    form = await request.form()\n    result = validate(form, {\n        \"name\":  [required, max_length(200)],\n        \"email\": [required, email],\n    })\n    if not result:\n        return ValidationError(\"contacts.html\", \"form\", errors=result.errors, form=form)\n    contacts.append(Contact(**result.data))\n    return Page(\"contacts.html\", \"list\", contacts=contacts)\n```\n\n`ValidationError` returns a 422 with the re-rendered form fragment so htmx\nswaps the error inline; non-htmx requests get the full page back.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eStreaming HTML\u003c/strong\u003e — Progressive rendering\u003c/summary\u003e\n\nKida renders template sections as they complete. The browser receives the shell immediately\nand content fills in progressively:\n\n```python\n@app.route(\"/dashboard\")\nasync def dashboard(request: Request):\n    return Stream(\"dashboard.html\",\n        header=site_header(),\n        stats=await load_stats(),\n        activity=await load_activity(),\n    )\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eServer-Sent Events\u003c/strong\u003e — Real-time HTML updates\u003c/summary\u003e\n\nPush Kida-rendered HTML fragments to the browser in real-time:\n\n```python\n@app.route(\"/notifications\")\nasync def notifications(request: Request):\n    async def stream():\n        async for event in notification_bus.subscribe(request.user):\n            yield Fragment(\"components/notification.html\", event=event)\n    return EventStream(stream())\n```\n\nCombined with htmx's SSE support, this enables real-time UI updates with zero client-side\nJavaScript. The server renders HTML, the browser swaps it in.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eMiddleware\u003c/strong\u003e — Composable request/response pipeline\u003c/summary\u003e\n\nNo base class. No inheritance. A middleware is anything that matches the protocol:\n\n```python\nasync def timing(request: Request, next: Next) -\u003e Response:\n    start = time.monotonic()\n    response = await next(request)\n    elapsed = time.monotonic() - start\n    return response.with_header(\"X-Time\", f\"{elapsed:.3f}\")\n\napp.add_middleware(timing)\n```\n\nBuilt-in middleware: CORS, StaticFiles, HTMLInject, Sessions, SecurityHeaders.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eTyped Contracts\u003c/strong\u003e — Compile-time hypermedia validation\u003c/summary\u003e\n\nChirp validates the server-client boundary at startup:\n\n```python\n# Prints a contract report and exits non-zero on errors.\napp.check()\n\n# Optional strict mode: treat warnings as failures too.\napp.check(warnings_as_errors=True)\n```\n\nEvery `hx-get`, `hx-post`, and `action` attribute in your templates is checked against the\nregistered route table. Every `Fragment` and `SSE` return type is checked against available\ntemplate blocks. SSE safety checks catch broken `sse-connect` / `sse-swap` structures and\nunsafe inherited target scopes before runtime.\n\nFor strict CI:\n\n```bash\nchirp check myapp:app --warnings-as-errors\n```\n\n\u003c/details\u003e\n\n---\n\n## Development\n\n```bash\ngit clone https://github.com/lbliii/chirp.git\ncd chirp\nuv sync --group dev\npytest\n```\n\n---\n\n## The Bengal Ecosystem\n\nA structured reactive stack written in pure Python for 3.14t free-threading. Chirp is the framework; packages like `chirp-ui` sit on top as optional companions.\n\n| | | | |\n|--:|---|---|---|\n| **ᓚᘏᗢ** | [Bengal](https://github.com/lbliii/bengal) | Static site generator | [Docs](https://lbliii.github.io/bengal/) |\n| **∿∿** | [Purr](https://github.com/lbliii/purr) | Content runtime | — |\n| **⌁⌁** | **Chirp** | Web framework ← You are here | [Docs](https://lbliii.github.io/chirp/) |\n| **ʘ** | [chirp-ui](https://github.com/lbliii/chirp-ui) | Optional companion UI layer | — |\n| **=^..^=** | [Pounce](https://github.com/lbliii/pounce) | ASGI server | [Docs](https://lbliii.github.io/pounce/) |\n| **)彡** | [Kida](https://github.com/lbliii/kida) | Template engine | [Docs](https://lbliii.github.io/kida/) |\n| **ฅᨐฅ** | [Patitas](https://github.com/lbliii/patitas) | Markdown parser | [Docs](https://lbliii.github.io/patitas/) |\n| **⌾⌾⌾** | [Rosettes](https://github.com/lbliii/rosettes) | Syntax highlighter | [Docs](https://lbliii.github.io/rosettes/) |\n| **⚡** | [Zoomies](https://github.com/lbliii/zoomies) | QUIC / HTTP/3 | — |\n\nPython-native. Free-threading ready. No npm required.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flbliii%2Fchirp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flbliii%2Fchirp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flbliii%2Fchirp/lists"}