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:
objectA Rational Web application, and an ASGI app (
uvicorn module:app).One origin serves pages at
/,@remoteservices underapi_prefix(default/api, with OpenAPI docs at/api/docs) and framework assets under/_rational. Run it withpython app.py --devor--prod(seerational.cli).Page views are held in process memory: run a single worker, or use sticky sessions.
- Parameters:
title (str)
description (str | None)
version (str)
theme_mode (ThemeMode)
lang (str)
breakpoints (Mapping[str, Breakpoint] | None)
containers (Mapping[str, Breakpoint] | None)
login_path (str | None)
debug (bool | None)
api_prefix (str)
api_docs (bool)
favicon (str | None)
secure_cookies (bool | None)
csp (bool)
session_store (SessionBackend | None)
session_ttl (float)
page_ttl (float)
max_pages (int)
event_burst (int)
event_rate (float)
max_event_bytes (int)
- page(path, *, title=None, auth=None)[source]¶
Register a
Pagesubclass atpath.{name}segments become typed props.
- service(cls)[source]¶
Register a
Service: each@remotemethod becomes a typed HTTP endpoint.- Parameters:
cls (T)
- Return type:
T
- include(router)[source]¶
Register everything a
Routerrecorded: 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 orNone; it runs once per request.
- run(argv=None, **overrides)[source]¶
Serve from the command line:
python app.py --devorpython app.py --prod.argvdefaults tosys.argv[1:]; keywordoverrides(mode,host,portand any uvicorn option) take precedence. Seerational.cli.
- commit_session(ctx)[source]¶
Apply a pending id rotation. Returns True if the browser needs a new cookie.
- 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]
- 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
Pagesubclass atpath(relative to the router’s prefix).
Components¶
- class rational.Component(*children, key=None, style=None, **props)[source]¶
Bases:
objectSubclass 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 '.'}")
Stateattributes declare reactive state. Positional arguments becomeself.children.key=gives a stable identity among siblings andstyle=is merged into the component’s root element.- mount()[source]¶
Called once, before the first render of this instance. May be async.
- Return type:
None | Awaitable[None]
- use(service)[source]¶
An instance of
servicebound 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
After this event, send the browser to
url(same-origin paths only).- Parameters:
url (str)
- Return type:
None
- class rational.Page(*children, key=None, style=None, **props)[source]¶
Bases:
ComponentA 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.
- 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
ButtonandButtonLink.
- 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.
contentmay include callables, evaluated now.
- rational.components.basic.ButtonLink(*content, href, variant='primary', size='md', style=None, key=None, **attrs)[source]¶
A link that looks like a button (navigation, not an action).
- rational.components.basic.Image(src, *, alt, width=None, height=None, lazy=True, style=None, key=None, **attrs)[source]¶
altis required; passalt=""for purely decorative images.
- rational.components.basic.Badge(*content, tone='neutral', style=None, key=None, **attrs)[source]¶
A small label for statuses, tags, categories or counts.
- 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.
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.
- 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 leastmin_column_widthpx as fit (no media queries).
- 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.
- rational.components.layout.Main(*children, style=None, key=None, **attrs)¶
A semantic <main> element.
A semantic <footer> element.
A semantic <nav> element.
- rational.components.layout.Section(*children, style=None, key=None, **attrs)¶
A semantic <section> element.
- rational.components.layout.Article(*children, style=None, key=None, **attrs)¶
A semantic <article> element.
- rational.components.layout.Aside(*children, style=None, key=None, **attrs)¶
A semantic <aside> element.
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.
- 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_inputhandlers receive the value (debounced).- Parameters:
label (str)
name (str | None)
type (str)
placeholder (str | None)
required (bool)
disabled (bool)
autocomplete (str | None)
help (str | None)
error (str | None)
hide_label (bool)
on_input (Callable[[...], Any] | ClientOp | list[Callable[[...], Any] | ClientOp] | None)
on_change (Callable[[...], Any] | ClientOp | list[Callable[[...], Any] | ClientOp] | None)
on_enter (Callable[[...], Any] | ClientOp | list[Callable[[...], Any] | ClientOp] | None)
style (Style | None)
attrs (Any)
- 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]¶
optionsare values or(value, label)pairs.
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.
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:
display (str | None)
layout (Literal['vertical', 'horizontal'] | None)
direction (str | None)
justify (str | None)
align (str | None)
align_self (str | None)
position (str | None)
z_index (int | None)
overflow (str | None)
overflow_x (str | None)
overflow_y (str | None)
font_family (str | None)
text_align (str | None)
text_transform (str | None)
text_decoration (str | None)
white_space (str | None)
color (str | None)
background (str | None)
border_color (str | None)
border_style (str | None)
outline (str | None)
shadow (str | None)
opacity (float | None)
cursor (str | None)
transform (str | None)
pointer_events (str | None)
user_select (str | None)
list_style (str | None)
object_fit (str | None)
container_type (str | None)
container_name (str | None)
hover (Style | None)
focus (Style | None)
focus_visible (Style | None)
active (Style | None)
disabled (Style | None)
placeholder (Style | None)
- class rational.styling.style.StyleError[source]¶
Bases:
ValueErrorAn 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:
- class rational.Breakpoint(min: 'int | None' = None, max: 'int | None' = None)[source]¶
Bases:
object
- class rational.styling.StyleCompiler(theme='modern', breakpoints=None, containers=None)[source]¶
Bases:
objectResolves tokens against a theme; caches compiled styles by value.
- Parameters:
breakpoints (Mapping[str, Breakpoint] | None)
containers (Mapping[str, Breakpoint] | None)
Services and data¶
- class rational.backend.Service(context)[source]¶
Bases:
objectGroup 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
GETonly for safe, idempotent reads.auth="required"rejects anonymous callers with 401.
- class rational.backend.database.Database(url, *, echo=False, **engine_options)[source]¶
Bases:
object- 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¶
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:
ExceptionBase for errors whose
messageis safe to show to the user.
- exception rational.errors.RemoteError(message='', *, code=None, status=None, details=None)[source]¶
Bases:
RationalErrorRaise from services or handlers to report a user-facing failure.
- exception rational.errors.InvalidInput(message='', *, code=None, status=None, details=None)[source]¶
Bases:
RationalErrorUser input that cannot be accepted.
detailslists{"field", "message"}items, which the browser runtime shows next to the matching form fields.
- exception rational.errors.FieldError(field, message='')[source]¶
Bases:
InvalidInputReject 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.
- exception rational.errors.Unauthorized(message='', *, code=None, status=None, details=None)[source]¶
Bases:
RationalError
- exception rational.errors.Forbidden(message='', *, code=None, status=None, details=None)[source]¶
Bases:
RationalError
- exception rational.errors.NotFound(message='', *, code=None, status=None, details=None)[source]¶
Bases:
RationalError
Rendering¶
Running¶
Command line: python app.py --dev | --prod and python -m rational <target> ....
Modes¶
--devauto-reload on code changes, live browser reload, readable error pages andoverlays, binds to 127.0.0.1. The default unless
RATIONAL_ENV=production.--prodone origin serving pages at/, the JSON API under/apiand frameworkassets under
/_rational; no debug output; honoursHOST/PORT; trusts proxy headers fromFORWARDED_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:
ExceptionThe 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.
- rational.cli.serve(app, argv=None, *, target=None, **overrides)[source]¶
Parse
argv(default:sys.argv[1:]) and runappwith uvicorn.
- rational.cli.main(argv=None)[source]¶
python -m rational path/to/app.py|module:attr [--dev|--prod] [options]
- rational.cli.supervise(target, args, host, port)[source]¶
Run the app in a child process and restart it whenever a Python file changes.
- rational.cli.watch(directory, interval=0.4)[source]¶
Yield the (relative) path of a changed
*.pyfile, forever.
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:
objectWhat
export_site()wrote and what it left out (with the reason).
- async rational.export.export_site(app, out, *, base_path='')[source]¶
Write the static pages and assets of
appunderout.base_pathis 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-pathdoes when serving.- Parameters:
- Return type:
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