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.