API reference#
Generated directly from Hayavega's own docstrings and type hints (the same
ones mypy checks in CI) — this page can't drift from the actual
public API the way a hand-maintained second copy would.
Routing#
path #
Register a route. The root route is written as an empty string.
hayavega.rest.api_route #
api_route(pattern: str, view: Callable, methods: Iterable[str] = ('GET',), *, secured: bool = False, request_schema: dict | None = None, responses: dict[int, dict] | None = None) -> Route
Create a JSON endpoint. Views may return a dict/list or a Response.
secured, request_schema, and responses describe the endpoint
for the generated OpenAPI document at /openapi.json. secured
marks the endpoint as requiring a Bearer session token. request_schema
is a JSON Schema dict for the request body. responses maps a status
code to {"description": str, "schema": dict} (schema is optional).
None of these change runtime behavior -- omit them and Hayavega documents
the endpoint with its previous generic fallback schema.
view may be async def when deployed under hayavega.asgi (see
DEPLOYMENT.md): the wrapper stays a coroutine function itself so the
ASGI runtime awaits it directly rather than running it in a thread.
Source code in src/hayavega/rest.py
Request & Response#
hayavega.request.Request
dataclass
#
Request(method: str, path: str, query_params: dict[str, list[str]], body: bytes, headers: dict[str, str], remote_addr: str = '', is_secure: bool = False, raw_query: str = '')
files #
Parse a multipart/form-data body into uploaded files, keyed
by field name (the first file, if more than one was submitted
under the same field name). Returns {} for any other content
type. Requires python-multipart -- see MultipartUnavailableError.
Source code in src/hayavega/request.py
form #
Parse a form body into plain fields -- last value wins per
field. Handlesapplication/x-www-form-urlencoded(the
default) andmultipart/form-data(needspython-multipart;
seefiles()`).
Source code in src/hayavega/request.py
hayavega.response.Response
dataclass
#
Response(body: str | bytes, status: int = 200, content_type: str = 'text/html; charset=utf-8', headers: tuple[tuple[str, str], ...] = ())
hayavega.response.json_response #
Models#
hayavega.models.Entity #
Source code in src/hayavega/models.py
collection
classmethod
#
signals, when given a SignalBus, publishes
"<entity>.created"/"<entity>.updated"/"<entity>.deleted"
from append/revise/erase -- opt-in, matching this
codebase's "opt-in, not on by default" policy elsewhere
(Hardening), so every existing caller's behavior is unchanged
when omitted. See docs/topics/signals.md.
Source code in src/hayavega/models.py
hayavega.models.Collection #
Source code in src/hayavega/models.py
prepare #
Ensure the entity's table exists.
Every fetch/total/revise/erase/append call goes
through here, so the "already prepared" case -- true for nearly
every call after the first -- must not pay for a second connection
and a CREATE TABLE IF NOT EXISTS round trip. Cached per
(table, database) for the life of the process instead: a table's
schema doesn't change without a migration, which is a separate,
deliberate operation this cache has no effect on.
Source code in src/hayavega/models.py
hayavega.models.Selection #
Selection(collection: Collection, clauses=(), ordering=(), limit: int | None = None, offset: int = 0, search_clause: tuple[tuple[str, ...], str] | None = None)
Source code in src/hayavega/models.py
search #
OR a LIKE match across fields -- a narrow, single-purpose
capability for admin's search_fields, not a general Q()/OR
composition system. Combines with match()'s clauses via AND
(seek(published=True).search("title", "body", query="rust")
means both). An empty query is a no-op, so a blank search
box doesn't filter anything out.
Source code in src/hayavega/models.py
values #
Fetch rows as plain dicts instead of Entity instances.
fetch() runs every column through its Column.coerce() and
builds a real Entity object per row -- the right default when the
caller wants entity behavior (further selection, .revise(), type
coercion). Read-only rendering (a list page, a JSON API response)
often immediately discards that object for a plain dict of a few
fields, paying for hydration it never uses. values() skips it:
pass specific column names to select only those, or none for every
column.
Source code in src/hayavega/models.py
Database#
hayavega.db.connect #
Source code in src/hayavega/db.py
hayavega.db.connect_readonly #
Open a connection scoped to reads only.
For SQLite, this opens the file with the mode=ro URI parameter instead
of a plain read-write connection. Measured (benchmarks/
sqlite_readonly_reads.py) at a real, consistent win on this project's
actual default (journal_mode=DELETE, untouched): ~25-30% faster per
call single-threaded and ~18-30% higher aggregate throughput under
16-thread contention, on Windows -- a read-only open skips the
reserved/pending-lock bookkeeping SQLite otherwise does on every
statement to guard against a writer, which a connection that will only
ever SELECT never needs.
Never pooled by default -- opened and closed fresh on every call, which
does not reproduce the many-threads-holding-a-connection-open
contention that made pooling regress the WSGI path under
enable_connection_pooling(). Pass through enable_readonly_connection_pooling()
to additionally reuse one read-only connection per thread; see that
function's docstring for why pooling read-only connections was found
not to carry the write-pooling regression.
Only used internally by Selection.fetch()/values()/total() --
pure-read call sites that never call .commit(). Collection.append()/
Selection.revise()/erase() are unaffected and keep using connect().
Falls back to a normal connect() if the read-only open fails (for
example, the database file does not exist yet) rather than raising --
in practice this never happens on the read path, since every read call
site calls Collection.prepare() first, which always creates the file
via a normal read-write connect() if it is missing.
Non-SQLite engines have no equivalent parameter tested here and fall
back to connect() unchanged.
Source code in src/hayavega/db.py
hayavega.db.enable_connection_pooling #
Reuse one connection per (thread, database config) instead of opening
a new one on every connect() call.
Off by default; every existing caller of connect() -- models.py,
auth.py, admin.py, migrations.py, production.py's readiness
check -- needs no change to benefit, since this only changes what
connect() and Connection.close() do internally, not their contracts.
Scoped deliberately to match Hayavega's WSGI/ASGI concurrency model: one
connection per worker thread, matching the one-request-per-thread (WSGI)
or one-coroutine-at-a-time-per-thread (ASGI sync views run on
create_asgi_application's dedicated max_sync_workers-sized thread
pool) model this framework already uses, rather than a general bounded
pool shared across threads with checkout/checkin
queuing. That keeps the mechanism simple and avoids the cross-thread
connection-sharing hazards DB-API connections generally aren't safe for
(SQLite enforces this directly: a connection created with the default
check_same_thread=True cannot be used from another thread at all).
A pooled MySQL/PostgreSQL connection that has sat idle long enough for
the server to drop it (MySQL's wait_timeout, a network blip) is
transparently reconnected: Connection.execute() recognizes the
driver-specific "connection is dead" error, reopens the underlying
connection, and retries the same statement once -- see
_is_stale_connection_error/Connection._reconnect. This does not
affect SQLite, which has no server-side idle timeout and is never
eligible for this path.
Benchmark this for your own deployment before enabling it -- it is
not a universal win. Measured against benchmarks/'s SQLite-backed
db_list/template_render scenarios: a real, substantial improvement
under hayavega.asgi + uvicorn (db_list +15-30%, and it is what pushed
template_render past FastAPI for the first time), but a consistent
~10-15% regression under the WSGI path (waitress, 16 threads) --
confirmed at both the full HTTP level (oha, multiple repeated runs)
and in an isolated multi-threaded microbenchmark with no framework code
involved at all, so this is a property of many threads holding a
persistent SQLite connection open concurrently under contention, not a
bug in this pool's own bookkeeping. benchmarks/hayavega_app/app.py
(WSGI) deliberately leaves this disabled for exactly that reason;
benchmarks/hayavega_asgi_app/app.py (ASGI) deliberately enables it.
The mechanism is identical in both -- only which concurrency model ends
up holding several connections open at once under load differs.
Source code in src/hayavega/db.py
hayavega.db.enable_readonly_connection_pooling #
Reuse one read-only connection per (thread, database config) instead
of opening a fresh one on every connect_readonly() call.
Independent of enable_connection_pooling() -- that function pools
read-write connections and, measured against this project's own
benchmarks/, regresses the WSGI path by ~10-15% (many threads holding
a read-write connection open contends under SQLite on this
environment; see its own docstring for the full result). A read-only
connection takes no reserved/pending locks at all, so holding one open
per thread was hypothesized not to reproduce that contention -- and
an isolated 16-thread microbenchmark confirmed the hypothesis at that
layer: pooling read-only connections beat both a fresh read-only
connection per call and a pooled read-write connection (which
reproduced the known regression, as a same-run sanity check) by
~27-43% across two runs. Only affects connect_readonly() -- the write
path (connect(), and therefore Collection.append()/
Selection.revise()/erase()) is completely unaffected by this flag
and stays governed by enable_connection_pooling() alone.
That isolated win did not survive to the HTTP level -- measured, not
assumed. Four paired oha -z 10s -c 30 runs against
benchmarks/hayavega_app/app.py under real waitress, with and without
this enabled: db_list averaged 811 req/s pooled vs. 840 req/s
unpooled; template_render averaged 708 req/s pooled vs. 735 req/s
unpooled -- both within this scenario's established run-to-run noise
band (up to ~17%, see INDUSTRY_LEADERSHIP_ROADMAP.md's
template_render re-measurement), so neither a real win nor a real
loss, not the substantial win the isolated microbenchmark predicted.
benchmarks/hayavega_app/app.py (WSGI) deliberately leaves this
disabled for exactly that reason: the sub-millisecond connection-layer
saving this recovers is proportionally invisible against a request
whose own sqlite3.connect()+.execute() already costs ~35-45ms
end to end on this environment -- the same "environment ceiling"
finding every other connection-layer optimization in this document has
run into. Kept available and tested (not reverted) since it is a real,
correct capability that may still help a different deployment's
workload shape; just not proven to help this one.
Benchmark your own deployment before enabling -- disclosed exactly
like every other tunable in this module. A pooled read-only SQLite
connection reads a point-in-time snapshot of the schema and any
long-lived cached state the driver keeps; this framework's own
Collection.prepare() cache already assumes a table's schema never
changes without a migration, so this carries no additional risk beyond
what enable_connection_pooling() already documents for MySQL/
PostgreSQL connections going stale after a long idle period.
Source code in src/hayavega/db.py
Authentication#
hayavega.auth.create_user #
create_user(database_path: str | Path | dict, username: str, email: str, password: str, *, is_superuser: bool = False, validators: Iterable[Callable[[str], None]] | None = None) -> User
validators, when given, replaces the default [validate_password]
entirely -- each is called as validator(password) and must raise
ValueError to reject it; the first rejection wins. Omit it for the
default policy.
Source code in src/hayavega/auth.py
hayavega.auth.authenticate #
authenticate(database_path: str | Path | dict, username: str, password: str, totp_code: str | None = None, *, ip_address: str | None = None, user_agent: str | None = None, secret_key: str | None = None) -> User | None
Verify credentials and record the attempt, including the caller-supplied IP address and user agent, if given.
secret_key is only needed when the user has TOTP enabled and their
secret was stored encrypted (see enable_totp) -- pass the same
Settings.secret_key used to enable it, or the TOTP check fails
closed rather than silently skipping it.
Source code in src/hayavega/auth.py
hayavega.auth.create_session #
create_session(database_path: str | Path | dict, user: User, duration_seconds: int = DEFAULT_SESSION_SECONDS, *, ip_address: str | None = None, user_agent: str | None = None) -> str
Source code in src/hayavega/auth.py
Runtime#
hayavega.runtime.create_application #
create_application(urlpatterns, *, hardening: HardeningOptions | None = None, debug: bool = False, signals: SignalBus | None = None, pulse: SignalBus | None = None, metrics: MetricsRegistry | None = None, api_title: str = 'Hayavega API', api_version: str = '0.1.0', pool_connections: bool = False, middleware: list | None = None)
Build the WSGI application for a route list.
pool_connections=True calls hayavega.db.enable_connection_pooling()
-- a convenience, not a scoped setting: pooling is process-global (one
connection reused per worker thread per database config), so it affects
every hayavega.db.connect() call in the process, not only requests
served by this particular application object. See
enable_connection_pooling's docstring for what this does and does
not solve before enabling it against MySQL/PostgreSQL specifically.
This is separated from serve so application behavior can be tested
without opening a network socket. signals, when given a SignalBus,
publishes request.started/request.finished asynchronously so a
slow subscriber cannot add latency to the response. pulse is a
deprecated alias for signals. metrics, when
given a MetricsRegistry, records one request-count and duration
observation per completed request, labeled by the matched route pattern;
pair it with hayavega.metrics.metrics_routes to expose /metrics.
api_title and api_version name the generated OpenAPI document at
/openapi.json; set api_version to version your API independently
of the package. middleware, when given a list of already-resolved
middleware factories (hayavega.middleware.load_middleware(Settings.middleware)),
wraps route matching and the view call -- it runs after hardening
(which always runs first and can't be bypassed by a middleware) and
before the matched view. See hayavega.middleware for the factory
contract.
Source code in src/hayavega/runtime.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
hayavega.asgi.create_asgi_application #
create_asgi_application(urlpatterns, *, hardening: HardeningOptions | None = None, debug: bool = False, signals: SignalBus | None = None, pulse: SignalBus | None = None, metrics: MetricsRegistry | None = None, api_title: str = 'Hayavega API', api_version: str = '0.1.0', pool_connections: bool = False, max_sync_workers: int = 16, websocket_patterns: list[WebSocketRoute] | None = None, middleware: list | None = None)
Build the ASGI application for a route list.
Accepts the same hardening/debug/signals/metrics/
api_title/api_version/pool_connections arguments as
runtime.create_application, with identical behavior (request
hardening, Signals events, metrics recording, OpenAPI/Swagger routes)
-- only the transport differs. Pooling is thread-local and sync views
run via a dedicated worker-thread pool (see max_sync_workers below),
so each of those threads gets its own reused connection across the
requests it handles, the same benefit WSGI's thread-per-request model
gets. pulse is a deprecated alias for signals, kept for parity
with the WSGI path.
max_sync_workers (default 16, matching this project's own documented
waitress --threads=16 guidance in DEPLOYMENT.md) sizes a
dedicated ThreadPoolExecutor that every synchronous view runs on,
instead of asyncio.to_thread's event-loop-wide default executor.
That default executor is sized min(32, os.cpu_count() + 4) --
tuned for occasional CPU-bound offloading, not for a whole
application's worth of concurrent I/O-bound sync views, and on a
modest machine (4 cores -> 8 workers) it becomes a real queuing
bottleneck under realistic concurrency well before the database or the
network does. An in-process microbenchmark for this
(benchmarks/asgi_executor_sizing.py) was tried first but proved too
noisy on this environment to trust on its own -- repeated runs
disagreed on which worker count won. The real evidence is the
HTTP-level before/after comparison in
benchmarks/README.md#asgi-vs-fastapi: a real uvicorn server
under real oha load, run twice before and twice after this change,
which was large and consistent in every run (e.g. json_echo roughly
doubled). Benchmark your own deployment's concurrency and core count
before assuming 16 is right for it, the same standing advice
enable_connection_pooling() and DEPLOYMENT.md already give for
their own tunables.
Pass websocket_patterns=[websocket_path(...), ...] (from
hayavega.websocket) to add WebSocket routes; see that module for the
handler contract. WebSocket connections have no WSGI equivalent, so this
parameter only ever does anything under this ASGI application. Reuses
hardening's allowed_hosts/allowed_origins to reject
cross-origin WebSocket connections the same way HTTP requests already
are, if either is configured.
middleware, when given a list of already-resolved middleware
factories (hayavega.middleware.load_middleware(Settings.middleware)),
wraps route matching and the view call for HTTP requests the same way
runtime.create_application does -- after hardening, before the
matched view. A middleware's returned handler must be async def
here (check inspect.iscoroutinefunction if it needs to support
both transports); it does not apply to WebSocket connections.
Source code in src/hayavega/asgi.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | |
hayavega.rust_runtime.serve #
serve(urlpatterns, host: str = '127.0.0.1', port: int = 8000, *, hardening: HardeningOptions | None = None, debug: bool = False, workers: int = 8, processes: int = 1, tls_cert: str | None = None, tls_key: str | None = None) -> None
Build the same WSGI application hayavega.runtime.serve would and run
it behind the native Rust server instead of wsgiref.
tls_cert/tls_key (PEM file paths) serve the application over HTTPS
instead of plain HTTP when both are given; both must be provided
together (hayavega_native.serve_wsgi rejects one without the other with
a clear error rather than silently serving plaintext). The framework's
own Request.is_secure, and therefore Hardening's FORCE_HTTPS/
secure-cookie logic, work correctly under TLS exactly as they do behind
a TLS-terminating reverse proxy -- wsgi.url_scheme is set to
"https" for every request served this way.
Raises RustRuntimeUnavailableError with an actionable message if
hayavega_native was not built for this install (e.g. cargo was not on
PATH during pip install, or HAYAVEGA_SKIP_NATIVE_BUILD=1 was set) --
the same situation every other hayavega_native-dependent feature in this
codebase (Signals' Rust dispatcher, mango_list) already handles by
falling back to a pure-Python equivalent; a WSGI server has no
equivalent fallback to fall back to within this same function, so the
caller (Settings.server.engine = 'rust' in settings, checked by
hayavega launch) is told plainly instead of silently starting the
default Python server under a different name.
processes (default 1, i.e. today's single-process behavior) runs
processes separate OS processes, each with its own Python interpreter
and GIL, all binding the same host:port via SO_REUSEPORT so the
kernel load-balances connections across them -- this is the real way to
use more than one CPU core: within a single process, CPython's GIL caps
Python view-code execution to one thread at a time no matter how large
workers is (measured directly: a workers sweep from 1 to 32 plateaus
after 2-4 workers on every CPU-bound route). SO_REUSEPORT has no real
equivalent on Windows (SO_REUSEADDR there does not load-balance the
same way), so processes > 1 raises RustRuntimeUnavailableError there
rather than silently falling back to single-process behavior. This is a
multi-process launcher, not a supervisor: a child process that exits
(crash or otherwise) is not restarted -- that is real, disclosed scope
this function does not cover yet (see NEXT_RELEASE_ROADMAP.md's
Phase 4 for the remaining supervision/restart work).