Source code for rational.router

"""``Router``: declare pages and services in their own modules, compose them in one place.

Like FastAPI's ``APIRouter`` or Flask's blueprints, a router only *records* what it is
given; ``App.include(router)`` registers everything. Modules never need to import the app
instance, so multi-file projects have no circular imports::

    # myapp/pages/admin.py
    router = Router(prefix="/admin")

    @router.page("/")              # served at /admin
    class AdminHome(Page): ...

    # app.py (the composition root)
    app = App()
    app.include(admin.router)
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, TypeVar

from .component import Page

P = TypeVar("P", bound=type[Page])
T = TypeVar("T", bound=type)


@dataclass(frozen=True, slots=True)
class PageEntry:
    path: str
    page: type[Page]
    title: str | None = None
    auth: str | None = None


[docs] @dataclass class Router: prefix: str = "" pages: list[PageEntry] = field(default_factory=list) services: list[type] = field(default_factory=list) startup: list[Callable[[], Any]] = field(default_factory=list) def __post_init__(self) -> None: self.prefix = "/" + self.prefix.strip("/") if self.prefix.strip("/") else ""
[docs] def page(self, path: str, *, title: str | None = None, auth: str | None = None) -> Callable[[P], P]: """Record a ``Page`` subclass at ``path`` (relative to the router's prefix).""" def decorator(cls: P) -> P: self.pages.append(PageEntry(join(self.prefix, path), cls, title, auth)) return cls return decorator
[docs] def service(self, cls: T) -> T: """Record a ``Service`` to expose its ``@remote`` methods.""" self.services.append(cls) return cls
def on_startup(self, fn: Callable[[], Any]) -> Callable[[], Any]: self.startup.append(fn) return fn
[docs] def include(self, other: Router) -> None: """Merge another router's records into this one, under this router's prefix.""" self.pages.extend(PageEntry(join(self.prefix, e.path), e.page, e.title, e.auth) for e in other.pages) self.services.extend(other.services) self.startup.extend(other.startup)
def join(prefix: str, path: str) -> str: """``join("/admin", "/")`` -> ``/admin``; ``join("/admin", "/users")`` -> ``/admin/users``.""" if not prefix: return path return prefix if path in ("", "/") else prefix + "/" + path.lstrip("/")