API reference

Application

class rational.App(*, title='Rational App', description=None, version='0.1.0', theme='modern', theme_mode='system', lang='en', database=None, breakpoints=None, containers=None, login_path=None, debug=None, api_prefix='/api', api_docs=True, favicon=None, secure_cookies=None, csp=False, session_store=None, session_ttl=86400, page_ttl=1800, max_pages=5000, event_burst=40, event_rate=20.0, max_event_bytes=65536)[source]

Bases: object

A Rational Web application, and an ASGI app (uvicorn module:app).

One origin serves pages at /, @remote services under api_prefix (default /api, with OpenAPI docs at /api/docs) and framework assets under /_rational. Run it with python app.py --dev or --prod (see rational.cli).

Page views are held in process memory: run a single worker, or use sticky sessions.

Parameters:
page(path, *, title=None, auth=None)[source]

Register a Page subclass at path. {name} segments become typed props.

Parameters:
  • path (str)

  • title (str | None)

  • auth (str | None)

Return type:

Callable[[P], P]

service(cls)[source]

Register a Service: each @remote method becomes a typed HTTP endpoint.

Parameters:

cls (T)

Return type:

T

include(router)[source]

Register everything a Router recorded: pages, services and startup hooks.

Parameters:

router (Router)

Return type:

None

authenticate(fn)[source]

Replace the default authenticator (session.user).

fn(context) (sync or async) returns the principal or None; it runs once per request.

Parameters:

fn (Callable[[Context], Any])

Return type:

Callable[[Context], Any]

run(argv=None, **overrides)[source]

Serve from the command line: python app.py --dev or python app.py --prod.

argv defaults to sys.argv[1:]; keyword overrides (mode, host, port and any uvicorn option) take precedence. See rational.cli.

Parameters:
Return type:

None

commit_session(ctx)[source]

Apply a pending id rotation. Returns True if the browser needs a new cookie.

Parameters:

ctx (Context)

Return type:

bool

finish(request, response, ctx)[source]

Persist session changes (creation, id rotation) into the response cookie.

Parameters:
  • request (Request)

  • response (Response)

  • ctx (Context)

Return type:

None

session_lock(ctx)[source]

Serialize requests that mutate a session shared by multiple pages.

Parameters:

ctx (Context)

Return type:

AsyncIterator[None]

dev_head(nonce, base='')[source]

The live-reload/error-overlay script tag, in debug mode only.

Parameters:
  • nonce (str | None)

  • base (str)

Return type:

str

class rational.Router(prefix: 'str' = '', pages: 'list[PageEntry]' = <factory>, services: 'list[type]' = <factory>, startup: 'list[Callable[[], Any]]'=<factory>)[source]

Bases: object

Parameters:
page(path, *, title=None, auth=None)[source]

Record a Page subclass at path (relative to the router’s prefix).

Parameters:
  • path (str)

  • title (str | None)

  • auth (str | None)

Return type:

Callable[[P], P]

service(cls)[source]

Record a Service to expose its @remote methods.

Parameters:

cls (T)

Return type:

T

include(other)[source]

Merge another router’s records into this one, under this router’s prefix.

Parameters:

other (Router)

Return type:

None

class rational.Context(app, request=None, session=None)[source]

Bases: object

Bound to one HTTP request (a page view or an event).

session is created lazily, so pages that never touch it set no cookie.

Parameters:
  • app (App | None)

  • request (Request | None)

  • session (Session | None)

property user: Any

The authenticated principal, or None.

set_user(user)[source]

Used by custom authenticators (App.authenticator).

Parameters:

user (Any)

Return type:

None

Components

class rational.Component(*children, key=None, style=None, **props)[source]

Bases: object

Subclass and implement render().

Class-level annotations declare props (constructor keywords):

class Greeting(Component):
    name: str
    excited: bool = False

    def render(self):
        return Text(f"Hello, {self.name}{'!' if self.excited else '.'}")

State attributes declare reactive state. Positional arguments become self.children. key= gives a stable identity among siblings and style= is merged into the component’s root element.

Parameters:
  • children (Any)

  • key (str | int | None)

  • style (Style | None)

  • props (Any)

mount()[source]

Called once, before the first render of this instance. May be async.

Return type:

None | Awaitable[None]

unmount()[source]

Called when the instance leaves the tree.

Return type:

None

use(service)[source]

An instance of service bound to the current request context.

Parameters:

service (type[S])

Return type:

S

touch(*names)[source]

Mark state as changed after mutating it in place.

Parameters:

names (str)

Return type:

None

navigate(url)[source]

After this event, send the browser to url (same-origin paths only).

Parameters:

url (str)

Return type:

None

run_js(*ops)[source]

After this event’s patches are applied, run declarative js.* operations.

Parameters:

ops (ClientOp)

Return type:

None

class rational.Page(*children, key=None, style=None, **props)[source]

Bases: Component

A routable component. Path parameters are declared as annotated props.

One instance exists per page view; it is the root of that view’s component tree.

Parameters:
  • children (Any)

  • key (str | int | None)

  • style (Style | None)

  • props (Any)

class rational.State(default=MISSING, *, factory=None, scope='component')[source]

Bases: Generic[T]

Declare reactive state.

scope="component" (default): one value per mounted component instance. scope="page": one value per page view, shared by all instances of the class. scope="session": one value per browser session, shared across tabs. Other tabs pick up changes on their next event.

Parameters:
  • default (T)

  • factory (Callable[[], T] | None)

  • scope (Scope)

Primitives

Text, headings, buttons, links and images.

rational.components.basic.button_style(variant='primary', size='md', *, full_width=False, style=None)[source]

The composed button look, shared by Button and ButtonLink.

Parameters:
  • variant (Literal['primary', 'secondary', 'outline', 'ghost', 'danger'])

  • size (Literal['sm', 'md', 'lg'])

  • full_width (bool)

  • style (Style | None)

Return type:

Style

rational.components.basic.Text(*content, tag='p', size=None, weight=None, color=None, muted=False, align=None, style=None, key=None, **attrs)[source]

A run of text. content may include callables, evaluated now.

Parameters:
  • content (Any)

  • tag (Literal['p', 'span', 'div', 'small', 'strong', 'em', 'label'])

  • size (str | None)

  • weight (int | str | None)

  • color (str | None)

  • muted (bool)

  • align (str | None)

  • style (Style | None)

  • key (str | int | None)

  • attrs (Any)

Return type:

Node

A link that looks like a button (navigation, not an action).

Parameters:
  • content (Any)

  • href (str)

  • variant (Literal['primary', 'secondary', 'outline', 'ghost', 'danger'])

  • size (Literal['sm', 'md', 'lg'])

  • style (Style | None)

  • key (str | int | None)

  • attrs (Any)

Return type:

Node

rational.components.basic.Image(src, *, alt, width=None, height=None, lazy=True, style=None, key=None, **attrs)[source]

alt is required; pass alt="" for purely decorative images.

Parameters:
Return type:

Node

rational.components.basic.Badge(*content, tone='neutral', style=None, key=None, **attrs)[source]

A small label for statuses, tags, categories or counts.

Parameters:
  • content (Any)

  • tone (Literal['neutral', 'primary', 'success', 'warning', 'danger'])

  • style (Style | None)

  • key (str | int | None)

  • attrs (Any)

Return type:

Node

rational.components.basic.Avatar(name, *, size=40, src=None, style=None, key=None, **attrs)[source]

A round picture, or the person’s initials when there is no src.

Parameters:
Return type:

Node

Layout primitives: thin wrappers over flexbox and grid, with responsive defaults.

rational.components.layout.Stack(*children, direction='vertical', gap='md', align=None, justify=None, wrap=False, collapse=None, tag='div', style=None, key=None, **attrs)[source]

Flex container. collapse="mobile" turns a horizontal stack vertical at that breakpoint.

Parameters:
Return type:

Node

rational.components.layout.Grid(*children, columns=None, min_column_width=240, gap='md', tag='div', style=None, key=None, **attrs)[source]

With columns=n: n equal columns, one column on mobile. Without: as many columns of at least min_column_width px as fit (no media queries).

Parameters:
Return type:

Node

rational.components.layout.Spacer(*, style=None)[source]

Takes the remaining space in a Row/Column.

Parameters:

style (Style | None)

Return type:

Node

rational.components.layout.Header(*children, style=None, key=None, **attrs)

A semantic <header> element.

Parameters:
Return type:

Node

rational.components.layout.Main(*children, style=None, key=None, **attrs)

A semantic <main> element.

Parameters:
Return type:

Node

rational.components.layout.Footer(*children, style=None, key=None, **attrs)

A semantic <footer> element.

Parameters:
Return type:

Node

rational.components.layout.Nav(*children, style=None, key=None, **attrs)

A semantic <nav> element.

Parameters:
Return type:

Node

rational.components.layout.Section(*children, style=None, key=None, **attrs)

A semantic <section> element.

Parameters:
Return type:

Node

rational.components.layout.Article(*children, style=None, key=None, **attrs)

A semantic <article> element.

Parameters:
Return type:

Node

rational.components.layout.Aside(*children, style=None, key=None, **attrs)

A semantic <aside> element.

Parameters:
Return type:

Node

Accessible form controls.

Every control has a visible (or screen-reader-only) <label>, a stable id, and an error slot wired through aria-describedby that the browser runtime fills when a submit handler’s payload fails validation.

rational.components.forms.Form(*children, on_submit=None, style=None, key=None, **attrs)[source]

A form whose submit sends all named fields to on_submit (no page reload).

Annotate the handler parameter with a Pydantic model to validate the data.

Parameters:
Return type:

Node

rational.components.forms.Input(label, *, name=None, value=None, type='text', placeholder=None, required=False, disabled=False, autocomplete=None, help=None, error=None, hide_label=False, on_input=None, on_change=None, on_enter=None, style=None, key=None, **attrs)[source]

A labelled text-like input. on_input handlers receive the value (debounced).

Parameters:
Return type:

Node

rational.components.forms.Select(label, options, *, name=None, value=None, required=False, help=None, error=None, hide_label=False, on_change=None, style=None, key=None, **attrs)[source]

options are values or (value, label) pairs.

Parameters:
Return type:

Node

rational.components.forms.Checkbox(label, *, name=None, checked=False, disabled=False, on_change=None, style=None, key=None, **attrs)[source]

on_change handlers receive True/False.

Parameters:
Return type:

Node

Events

Event payloads and declarative browser-local operations.

Python never runs in the browser. js.* builds a small, closed set of operations that the runtime executes locally, with no server round trip. Targets are CSS selectors; None means the element itself and "closest:<selector>" searches its ancestors.

class rational.events.Event(name, value=None, key=None, form=<factory>)[source]

Bases: object

Generic payload for handlers annotated with event: Event.

Parameters:
class rational.events.ClientOp(op: 'str', args: 'tuple[str | None, ...]')[source]

Bases: object

Parameters:

Styling

class rational.Style(display: 'str | None' = None, layout: "Literal['vertical', 'horizontal'] | None" = None, direction: 'str | None' = None, wrap: 'bool | str | None' = None, justify: 'str | None' = None, align: 'str | None' = None, align_self: 'str | None' = None, gap: 'Length | None' = None, row_gap: 'Length | None' = None, column_gap: 'Length | None' = None, grow: 'int | float | None' = None, shrink: 'int | float | None' = None, basis: 'Length | None' = None, columns: 'int | str | None' = None, rows: 'int | str | None' = None, column_span: 'int | str | None' = None, position: 'str | None' = None, top: 'Length | None' = None, right: 'Length | None' = None, bottom: 'Length | None' = None, left: 'Length | None' = None, inset: 'Length | None' = None, z_index: 'int | None' = None, overflow: 'str | None' = None, overflow_x: 'str | None' = None, overflow_y: 'str | None' = None, padding: 'Length | None' = None, padding_x: 'Length | None' = None, padding_y: 'Length | None' = None, padding_top: 'Length | None' = None, padding_right: 'Length | None' = None, padding_bottom: 'Length | None' = None, padding_left: 'Length | None' = None, margin: 'Length | None' = None, margin_x: 'Length | None' = None, margin_y: 'Length | None' = None, margin_top: 'Length | None' = None, margin_right: 'Length | None' = None, margin_bottom: 'Length | None' = None, margin_left: 'Length | None' = None, width: 'Length | None' = None, height: 'Length | None' = None, min_width: 'Length | None' = None, max_width: 'Length | None' = None, min_height: 'Length | None' = None, max_height: 'Length | None' = None, aspect_ratio: 'str | float | None' = None, font_family: 'str | None' = None, font_size: 'Length | None' = None, font_weight: 'int | str | None' = None, line_height: 'float | str | None' = None, letter_spacing: 'Length | None' = None, text_align: 'str | None' = None, text_transform: 'str | None' = None, text_decoration: 'str | None' = None, white_space: 'str | None' = None, color: 'Color | None' = None, background: 'Color | None' = None, border: 'bool | str | None' = None, border_top: 'bool | str | None' = None, border_right: 'bool | str | None' = None, border_bottom: 'bool | str | None' = None, border_left: 'bool | str | None' = None, border_color: 'Color | None' = None, border_width: 'Length | None' = None, border_style: 'str | None' = None, border_radius: 'Length | None' = None, outline: 'str | None' = None, shadow: 'str | None' = None, opacity: 'float | None' = None, cursor: 'str | None' = None, transition: 'bool | str | None' = None, transform: 'str | None' = None, pointer_events: 'str | None' = None, user_select: 'str | None' = None, list_style: 'str | None' = None, object_fit: 'str | None' = None, container_type: 'str | None' = None, container_name: 'str | None' = None, hover: 'Style | None' = None, focus: 'Style | None' = None, focus_visible: 'Style | None' = None, active: 'Style | None' = None, disabled: 'Style | None' = None, placeholder: 'Style | None' = None, viewport: 'Mapping[str, Style] | None' = None, container: 'Mapping[str, Style] | None' = None, raw: 'Mapping[str, str] | None' = None)[source]

Bases: object

Parameters:
items()[source]

The set (non-None) fields, in declaration order.

Return type:

tuple[tuple[str, Any], …]

key()[source]

A hashable value identity: equal styles have equal keys.

Return type:

tuple[Any, …]

class rational.styling.style.StyleError[source]

Bases: ValueError

An invalid or unsafe style value.

class rational.Theme(name: 'str', light: 'Mapping[str, str]', dark: 'Mapping[str, str]', space: 'Mapping[str, str]'=<factory>, radius: 'Mapping[str, str]'=<factory>, font_size: 'Mapping[str, str]'=<factory>, fonts: 'Mapping[str, str]'=<factory>, shadows: 'Mapping[str, str]'=<factory>, motion: 'str' = '160ms', density: 'float' = 1.0)[source]

Bases: object

Parameters:
with_colors(*, light=None, dark=None)[source]

Return a copy with some color tokens overridden (or new ones added).

Parameters:
Return type:

Theme

class rational.Breakpoint(min: 'int | None' = None, max: 'int | None' = None)[source]

Bases: object

Parameters:
  • min (int | None)

  • max (int | None)

class rational.styling.StyleCompiler(theme='modern', breakpoints=None, containers=None)[source]

Bases: object

Resolves tokens against a theme; caches compiled styles by value.

Parameters:

Services and data

class rational.backend.Service(context)[source]

Bases: object

Group related server operations. Instances are bound to one request context.

Parameters:

context (Context)

rational.backend.remote(fn=None, *, method='POST', auth=None, path=None)[source]

Expose a service method as an HTTP endpoint.

Use GET only for safe, idempotent reads. auth="required" rejects anonymous callers with 401.

Parameters:
  • fn (F | None)

  • method (Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])

  • auth (Literal['required'] | None)

  • path (str | None)

Return type:

Any

class rational.backend.database.Database(url, *, echo=False, **engine_options)[source]

Bases: object

Parameters:
  • url (str)

  • echo (bool)

  • engine_options (Any)

session()[source]

async with db.session() as session: ... — commit explicitly.

Return type:

AsyncSession

transaction()[source]

A session inside a transaction: commits on success, rolls back on error.

Return type:

AsyncIterator[AsyncSession]

Sessions

class rational.session.Session(sid)[source]

Bases: MutableMapping[str, Any]

Per-browser key/value data. Reads during render are tracked like state.

Parameters:

sid (str)

login(user)[source]

Store the authenticated principal and rotate the session id (anti-fixation).

Parameters:

user (Any)

Return type:

None

class rational.session.SessionBackend(*args, **kwargs)[source]

Bases: Protocol

What App needs from a session store; implement it to plug in Redis, a database, etc.

Errors

Exceptions that cross the client/server boundary as structured, safe errors.

exception rational.errors.RationalError(message='', *, code=None, status=None, details=None)[source]

Bases: Exception

Base for errors whose message is safe to show to the user.

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.RemoteError(message='', *, code=None, status=None, details=None)[source]

Bases: RationalError

Raise from services or handlers to report a user-facing failure.

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.InvalidInput(message='', *, code=None, status=None, details=None)[source]

Bases: RationalError

User input that cannot be accepted. details lists {"field", "message"} items, which the browser runtime shows next to the matching form fields.

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.FieldError(field, message='')[source]

Bases: InvalidInput

Reject one or more form fields from a handler or service:

raise FieldError("email", "This email is already registered")
raise FieldError({"password": "Too short", "confirm": "Does not match"})

Each message is shown under the matching form field; the JSON API answers 422.

Parameters:
Return type:

None

exception rational.errors.Unauthorized(message='', *, code=None, status=None, details=None)[source]

Bases: RationalError

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.Forbidden(message='', *, code=None, status=None, details=None)[source]

Bases: RationalError

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.NotFound(message='', *, code=None, status=None, details=None)[source]

Bases: RationalError

Parameters:
  • message (str)

  • code (str)

  • status (int)

  • details (Any)

Return type:

None

exception rational.errors.Redirect(url, status=303)[source]

Bases: Exception

Raise during a page render or an event to send the browser elsewhere.

Parameters:
Return type:

None

rational.errors.redirect(url)[source]

raise redirect("/login")

Parameters:

url (str)

Return type:

Redirect

Rendering

rational.h(tag, *children, key=None, style=None, **kwargs)[source]

Build an element.

on_<event>= accepts a server handler, a js.* client operation or a list mixing both. Every other keyword is an HTML attribute (see attr_name).

Parameters:
  • tag (str)

  • children (Any)

  • key (str | int | None)

  • style (Style | None)

  • kwargs (Any)

Return type:

Node

rational.unsafe_html(html)[source]

Insert html verbatim. The caller is responsible for sanitizing it.

Parameters:

html (str)

Return type:

RawHTML

Running

Command line: python app.py --dev | --prod and python -m rational <target> ....

Modes

--dev auto-reload on code changes, live browser reload, readable error pages and

overlays, binds to 127.0.0.1. The default unless RATIONAL_ENV=production.

--prod one origin serving pages at /, the JSON API under /api and framework

assets under /_rational; no debug output; honours HOST/PORT; trusts proxy headers from FORWARDED_ALLOW_IPS (default 127.0.0.1).

Hot reload uses a small supervisor rather than uvicorn’s reloader (which can hang on Windows): the app runs in a child process that restarts whenever a *.py file changes.

--export DIR does not serve at all: it writes the app’s static pages to DIR as plain HTML (see rational.export).

Page views live in process memory, so --workers > 1 needs sticky sessions.

exception rational.cli.TargetError[source]

Bases: Exception

The requested target could not be loaded as a Rational app.

rational.cli.export(app, directory, root_path='')[source]

Run a static export and print what was written and skipped. Returns the exit code.

Parameters:
Return type:

int

rational.cli.serve(app, argv=None, *, target=None, **overrides)[source]

Parse argv (default: sys.argv[1:]) and run app with uvicorn.

Parameters:
  • app (App)

  • argv (Sequence[str] | None)

  • target (Target | None)

  • overrides (Any)

Return type:

None

rational.cli.main(argv=None)[source]

python -m rational path/to/app.py|module:attr [--dev|--prod] [options]

Parameters:

argv (Sequence[str] | None)

Return type:

None

rational.cli.supervise(target, args, host, port)[source]

Run the app in a child process and restart it whenever a Python file changes.

Parameters:
Return type:

None

rational.cli.watch(directory, interval=0.4)[source]

Yield the (relative) path of a changed *.py file, forever.

Parameters:
Return type:

Iterator[str]

Static export: render an app’s static pages to plain HTML files.

python app.py --export site/ writes site/index.html, site/<path>/index.html, the theme stylesheet and every directory mounted with app.static. The result can be hosted on any static file host (GitHub Pages, Netlify, S3, …).

Only pages that are fully static can be exported: no path parameters, no sign-in requirement and no interactivity (an interactive page needs the live server for its events). Everything else is reported as skipped rather than exported broken.

class rational.export.ExportReport(pages=<factory>, assets=<factory>, skipped=<factory>)[source]

Bases: object

What export_site() wrote and what it left out (with the reason).

Parameters:
async rational.export.export_site(app, out, *, base_path='')[source]

Write the static pages and assets of app under out.

base_path is the URL prefix the site will be served under ("/my-repo" for a GitHub Pages project site); it prefixes every asset link, exactly like --root-path does when serving.

Parameters:
Return type:

ExportReport

Testing

Test pages without a browser.

client = TestClient(app) page = client.open(“/”) page.click(“Increment”) assert “Count: 1” in page.text

Events go through the real HTTP endpoint (cookies, CSRF, validation); patches are applied to a lightweight DOM so later lookups see the updated page.

exception rational.testing.EventFailedError[source]

Bases: AssertionError