Reference guide · Pyrora 0.1.0

Make a small app.
Keep the architecture open.

Pyrora is an alpha Python framework built on Starlette. Its core is intentionally narrow: ASGI, templates, routing, lifecycle, services, plugins, and in-process realtime primitives.

Compatibility: Python 3.10 or newer. The core depends only on Starlette and Jinja2.

01 / Quick start

Install and run a first app

Create an environment, install the optional development server, and start a project.

pip install "pyrora[server]"
pyrora new blog
cd blog
pyrora serve --reload

The generated application starts with an HTML page and an API health endpoint. A handwritten app can be as small as this:

from pyrora import Pyrora, json, render

app = Pyrora(debug=True)

@app.get("/")
async def home(request):
    return render(request, "home.html", title="Hello, Pyrora")

@app.get("/api/health")
async def health(request):
    return json({"ok": True})

02 / Routing & controllers

Route normal functions and coroutines

get, post, put, patch, and delete are thin route decorators. Route names and arbitrary metadata are available to tooling through app.route_metadata.

@app.route("/reports", methods=["GET", "POST"], name="reports")
async def reports(request):
    return {"ok": True}

@app.post("/notes", name="notes.create", metadata={"auth": "member"})
def create_note(request):
    return json({"created": True}, status_code=201)

Group modular endpoints with a Router.

from pyrora import Router

api = Router(prefix="/v1")

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

app.include_router(api, prefix="/api")

Controllers

Controller method names map directly to HTTP methods. A missing method receives Starlette’s 405 response with an Allow header.

from pyrora import Controller, json

class HealthController(Controller):
    async def get(self):
        return json({"status": "healthy"})

app.controller("/health", HealthController)

Responses

Use json(data, status_code=200), html(content), text(content), and redirect(url, status_code=303). Mappings and lists returned directly from an endpoint are encoded as JSON; strings become HTML responses.

03 / Templates & static files

Server-rendered by default

Pyrora discovers templates/ and static/ beneath the application root. Static assets are mounted at /static. Jinja2 autoescaping is enabled for HTML templates.

@app.get("/profile")
async def profile(request):
    return render(
        request,
        "profile.html",
        title="Profile",
        user=current_user,
    )

Plugins can add templates without touching your application folder:

app.add_template_directory("path/to/plugin/templates")
app.mount_static("/plugin-assets", "path/to/plugin/static", name="plugin-assets")

04 / Configuration & services

Explicit state, local to your app

Config merges defaults, a .env file, then OS environment variables. Later sources win. Values remain strings unless you request a cast.

from pyrora import Config

config = Config({"APP_NAME": "Notes"})
config.get("APP_DEBUG", cast=bool)
config.require("SECRET_KEY")
config.plugin("billing").get("currency")

The service container belongs to one Pyrora instance—there is no hidden global registry.

app.container.bind("mailer", Mailer)       # a new instance on each resolve
app.container.singleton("cache", Cache)    # one lazy instance
app.container.instance("settings", settings)

mailer = app.container.resolve("mailer")

Resolving an unregistered name raises a ServiceNotFoundError that lists available services.

05 / Plugins

Providers are installed on purpose

Plugins are never loaded merely because they are installed in the Python environment. Add one at application setup, and Pyrora will validate names and declared dependencies.

from pyrora.plugins import Plugin

class BillingPlugin(Plugin):
    name = "billing"
    version = "0.1.0"
    dependencies = ()
    config_defaults = {"BILLING_CURRENCY": "USD"}

    def register(self, app):
        app.container.singleton("billing", BillingService)
        app.add_route("/billing/health", self.health)
        app.add_command("billing:status", self.status)
        app.add_health_check("billing", self.check)

    async def boot(self, app):
        await self.connect()

app.install(BillingPlugin())

During register, a plugin may add services, routes, middleware, commands, event listeners, template paths, static paths, configuration defaults, health checks, and startup or shutdown work. Its optional async boot hook runs once at ASGI startup.

Third-party packages can publish an entry point in the pyrora.plugins group. pyrora plugins discover lists those entry points without importing them; loading one remains an explicit application action.

06 / WebSockets & WebRTC

Realtime without a mandatory broker

WebSocket handlers receive Starlette’s WebSocket object, including accept(), send_text(), send_json(), receive_text(), receive_json(), and close().

@app.websocket("/ws/chat")
async def chat(socket):
    await socket.accept()
    while True:
        message = await socket.receive_json()
        await socket.send_json({"echo": message})

Use app.connections for in-memory rooms. Pyrora removes disconnected sockets from rooms automatically.

app.connections.join("support", socket)
await app.connections.broadcast("support", {"event": "message", "data": data})
app.connections.leave("support", socket)

WebRTC signaling

from pyrora.realtime import WebRTCSignaling

signaling = WebRTCSignaling(app.connections)
app.websocket("/signaling/{room_id}")(signaling.endpoint)

WebRTCSignaling forwards room join/leave notices, offers, answers, ICE candidates, and hang-up events. It is only a signaling endpoint: browser peers carry audio and video directly. Pyrora does not relay or record media. Production calls commonly need STUN/TURN such as Coturn; larger products may use LiveKit, mediasoup, or other media infrastructure.

07 / Optional React preset

Bring React only when it earns its place

pyrora new realtime-app --react
cd realtime-app
pyrora frontend install
pyrora frontend dev

The preset creates a TypeScript Vite application with a responsive shell, API helper, WebSocket client, native WebRTC signaling helper, chat example, and room example. Vite proxies API and realtime paths in development; the generated backend allows the Vite origin with CORS.

pyrora frontend build

For same-origin production delivery, point Pyrora at the compiled output:

app = Pyrora(
    frontend_dist="frontend/dist",
    spa_fallback=True,
)

08 / CLI

Useful commands

pyrora new blogCreate a server-rendered project. pyrora new dashboard --reactCreate a backend plus optional React preset. pyrora serve --reloadRun app:app through Uvicorn. pyrora serve --app backend.app:appChoose a module target explicitly. pyrora routesList named routes and methods. pyrora plugins listList explicitly installed plugins. pyrora plugins discoverList entry points without loading code. pyrora make:controller UserControllerCreate a controller file. pyrora make:view dashboard/homeCreate a Jinja template.

09 / Current limits

What the alpha does not provide yet

Pyrora has no built-in database, authentication, queues, mailer, billing system, admin panel, Redis broadcaster, Socket.IO transport, media relay, or TURN service. Those capabilities should arrive as separate integrations so applications do not inherit dependencies they do not use.

The included room backend is in-memory and process-local. It is suitable for a single ASGI worker; a future Redis or Socket.IO adapter can provide cross-worker distribution without changing endpoint code.

For contributor setup, run pip install -e ".[dev,server]", then pytest and ruff check src tests. The repository includes routing, template, configuration, container, plugin, realtime, signaling, generator, SPA, and CLI coverage.

Read the repository