Operations¶
Health checks, maintenance mode, structured logging, error monitoring (Sentry), caching, and file storage.
health ¶
A /up health-check endpoint -- what load balancers, container
orchestrators (Kubernetes liveness/readiness probes), and uptime monitors
expect an app to expose. Nothing here is optional infrastructure a real
deployment can skip; this is the one thing every one of them needs.
HealthCheckServiceProvider ¶
Bases: ServiceProvider
Registers a health-check endpoint (/up by default).
Reports {"status": "ok", "checks": {...}} with a 200, or
{"status": "error", "checks": {...}} with a 503 if any check
fails -- the status code is what a load balancer/orchestrator actually
acts on, so a monitoring tool never needs to parse the body just to know
whether to route traffic here.
Currently checks database connectivity (a real SELECT 1, not just
"is a URL configured") when :class:~zeython.db.Database is bound in
the container -- skipped entirely for an app with no database.
HEALTH_CHECK_ENABLED-- defaulttrue; setfalseto turn the endpoint off entirely (e.g. if you don't want it publicly reachable and probe something else internally instead).HEALTH_CHECK_PATH-- default/up.
Source code in src/zeython/providers.py
maintenance ¶
Maintenance mode: take the whole app offline for a deploy or a risky
migration without stopping the process. zeython down writes a flag file
this middleware checks on every request; zeython up removes it. Mirrors
Laravel's artisan down/up closely, including the bypass-secret
mechanism for checking the site while it's "down" for everyone else.
MaintenanceModeMiddleware ¶
Pure ASGI middleware: while the flag file is present, every request
gets a 503 -- except one from an allowed IP, or one carrying a
valid bypass (a cookie set by visiting /<secret> once).
Reads the flag file fresh on every request rather than caching its
contents in memory: zeython up removing the file must take effect on
the very next request, not after a process restart, and the read
itself is cheap -- a single Path.exists() call is the entire cost
once the app isn't down.
Source code in src/zeython/maintenance.py
MaintenanceModeServiceProvider ¶
Bases: ServiceProvider
Registers :class:MaintenanceModeMiddleware.
Safe to always register, the same reasoning
:class:~zeython.request_id.RequestIdServiceProvider relies on: with
no flag file present (the default), every request pays one
Path.exists() call and nothing else changes. zeython down
creates that file; zeython up removes it -- no process restart
needed either way, since the middleware reads it fresh every time.
Register this last, after every other provider that adds
middleware (DatabaseServiceProvider included) -- the most recently
registered middleware wraps outermost, and maintenance mode needs to
intercept a request before anything else runs, including opening a
database session. That matters most exactly when this feature is
useful: a migration in progress, a database that's briefly down.
MAINTENANCE_STORE_PATH-- defaultstorage/framework/down.json, relative to the project root.
Source code in src/zeython/providers.py
maintenance_store_path ¶
Resolve the flag-file path, relative to base_path unless already absolute.
Source code in src/zeython/maintenance.py
enable_maintenance_mode ¶
enable_maintenance_mode(
store_path: Path,
*,
message: str | None = None,
retry: int | None = None,
allowed_ips: list[str] | None = None,
secret: str | None = None,
) -> str
Write the maintenance flag file. Returns the bypass secret in effect (either the one passed in, or a freshly generated one).
Source code in src/zeython/maintenance.py
disable_maintenance_mode ¶
Remove the maintenance flag file. Returns False if it wasn't there.
logging ¶
Structured (JSON) logging -- an opt-in alternative to the framework's default human-readable log line, for shipping logs to something that parses JSON (Datadog, ELK/Logstash, CloudWatch Logs Insights, Splunk) instead of grepping text. See docs/observability.md.
JsonFormatter ¶
Bases: Formatter
Renders one JSON object per line: timestamp, level, logger,
message, request_id (present whenever
:class:~zeython.request_id.RequestIdServiceProvider is registered --
"-" outside a request, same convention as the default text format),
exception (the formatted traceback, only when the record carries
one), plus any extra fields passed via logger.info(..., extra={...}).
error_monitoring ¶
Optional error monitoring (Sentry): unhandled request exceptions, jobs
that exhaust their retries, and scheduled tasks that raise all get
reported automatically once configured -- not just logged and forgotten
in a file nobody tails. Requires the sentry extra: pip install
zeython[sentry]. See docs/error-monitoring.md.
Deliberately not a hard dependency: every call in this module is a no-op
if sentry_sdk isn't installed or :func:init_sentry was never called,
so :func:report_exception is always safe to call unconditionally from
framework code (:mod:zeython.exceptions, :mod:zeython.queue,
:mod:zeython.schedule) without those modules taking on a hard
dependency on an optional extra.
ErrorMonitoringServiceProvider ¶
Bases: ServiceProvider
Initializes Sentry from SENTRY_DSN -- not registered by default,
and a no-op register() if SENTRY_DSN isn't set, so it's safe to
always register even in dev/test environments that don't have one::
# main.py
app.register(ErrorMonitoringServiceProvider(app))
Configurable via .env:
SENTRY_DSN-- required to do anything at all.SENTRY_TRACES_SAMPLE_RATE-- default0.0(errors only, no performance tracing).APP_ENV/app.envand a git-derived or manually-set release are passed through asenvironment/releaseif you setSENTRY_RELEASE.
Source code in src/zeython/providers.py
init_sentry ¶
init_sentry(
dsn: str,
*,
environment: str | None = None,
release: str | None = None,
traces_sample_rate: float = 0.0,
) -> None
Initialize the Sentry SDK. Raises ImportError with an install
hint if sentry_sdk isn't installed -- unlike :func:report_exception,
this is only ever called once you've explicitly opted in (a non-empty
SENTRY_DSN), so failing loudly here is correct: a typo'd DSN with a
silently-absent SDK would otherwise look like "no errors happened."
Source code in src/zeython/error_monitoring.py
report_exception ¶
Report exc to Sentry, tagged with tags -- a no-op if
:func:init_sentry was never called (including if sentry_sdk isn't
installed at all), so every call site here is safe regardless of
whether error monitoring is configured.
Source code in src/zeython/error_monitoring.py
cache ¶
Caching: an in-memory TTL cache by default, and remember() for the
common "check cache, else compute and store" pattern.
Like :class:~zeython.rate_limit.RateLimiter, the default backend is
process-local — correct for a single worker, and a real (if common)
limitation once you run multiple processes or machines, where each would
cache independently. :class:RedisCache is the opt-in, shared alternative.
Cache ¶
Bases: ABC
Get/put/forget keyed values, with optional per-entry expiry.
get
abstractmethod
async
¶
put
abstractmethod
async
¶
Store value under key. ttl is seconds until expiry; None never expires.
forget
abstractmethod
async
¶
has
abstractmethod
async
¶
flush
abstractmethod
async
¶
remember
async
¶
Return the cached value for key, computing and storing it via callback on a miss.
The common get-or-compute pattern in one call::
posts = await cache.remember("posts:recent", 60, lambda: Post.all())
callback only runs on a miss — a cache hit never calls it.
Source code in src/zeython/cache.py
InMemoryCache ¶
Bases: Cache
A process-local dict cache, correct and simple.
Expired entries are evicted lazily, on access — there's no background sweep, so an entry that's put and never read again sits in memory until the process restarts. Fine for typical cache sizes (route/query results, computed aggregates); not a fit for caching unboundedly many distinct keys.
Source code in src/zeython/cache.py
RedisCache ¶
Bases: Cache
A Redis-backed :class:Cache, shared across every process/machine
pointed at the same Redis — the limitation :class:InMemoryCache's
docstring names. Requires the redis extra (pip install zeython[redis]).
Values are JSON-encoded to cross the network as bytes. Unlike
InMemoryCache, which can hold any Python object in process memory,
only JSON-safe values (dict/list/str/int/float/
bool/None) survive the round trip here — cache a model's
to_dict(), not the model instance itself.
All keys are namespaced under prefix (default "zeython:cache:"),
and :meth:flush only clears that namespace (via SCAN, not
FLUSHDB) — safe to point at a Redis instance shared with other
subsystems (sessions, rate limiting) without wiping their data too.
Source code in src/zeython/cache.py
CacheServiceProvider ¶
Bases: ServiceProvider
Binds a :class:Cache into the container. :class:InMemoryCache (process-local) by default.
For a shared cache, bind :class:RedisCache instead of registering
this provider::
app.container.singleton(Cache, lambda: RedisCache(config.get("redis.url")))
Source code in src/zeython/providers.py
storage ¶
File storage: a small backend-agnostic abstraction, local filesystem by default, S3-compatible object storage as an opt-in extra.
The interesting part isn't the storage backend — it's :func:store_upload,
which is where uploads actually get dangerous if you're not careful: client
filenames are untrusted input, so the original name is never used as a
storage path (that's how you get path traversal or overwritten files), and
extension/size are checked before a single byte is written.
StoredFile
dataclass
¶
Metadata returned after a file has been written to storage.
Storage ¶
Bases: ABC
Abstract file storage backend.
url
abstractmethod
¶
temporary_url
abstractmethod
¶
A signed URL that grants access to key for expires_in seconds, then stops
working -- for a private file (an invoice, a user upload) you don't want reachable
from :meth:url forever, without standing up your own auth check in front of it.
Source code in src/zeython/storage.py
LocalStorage ¶
Bases: Storage
Stores files on the local filesystem, under root.
Every key is resolved against root and checked to still be inside
it — a key like "../../etc/passwd" raises rather than escaping the
storage directory.
Source code in src/zeython/storage.py
verify_temporary_url_token ¶
The storage key token grants access to, or None if it's missing,
tampered with, or past its expires_in. Used by the .../signed/{token}
route :class:StorageServiceProvider registers -- not meant to be called
directly in application code.
Source code in src/zeython/storage.py
S3Storage ¶
S3Storage(
bucket: str,
*,
region: str | None = None,
endpoint_url: str | None = None,
public_base_url: str | None = None,
)
Bases: Storage
S3-compatible object storage. Requires the s3 extra: pip install zeython[s3].
Works against AWS S3 and any S3-compatible service (MinIO, Cloudflare
R2, DigitalOcean Spaces, ...) by passing endpoint_url.
Source code in src/zeython/storage.py
StorageServiceProvider ¶
Bases: ServiceProvider
Binds a :class:Storage backend into the container — local filesystem by default.
.env configuration:
STORAGE_PATH— local storage root (default:<project>/storage/app)STORAGE_URL_PREFIX— default:/storageSTORAGE_SERVE_LOCALLY— mount the storage directory for direct GET access during development (default:true; turn off once you serve uploads from a CDN/reverse proxy in production)
Also registers the route :meth:LocalStorage.temporary_url links point
at (<url_prefix>/signed/<token>) — independent of
STORAGE_SERVE_LOCALLY, since that's the point of a signed URL: a way
to hand out time-limited access to a specific file without making the
whole storage directory public. Requires APP_SECRET_KEY to be set
(only enforced the first time you actually call temporary_url(), not
at boot).
For S3, construct and bind an :class:S3Storage yourself instead of
registering this provider::
app.container.singleton(Storage, lambda: S3Storage("my-bucket"))
Source code in src/zeython/providers.py
store_upload
async
¶
store_upload(
storage: Storage,
upload: UploadFile,
*,
directory: str = "",
allowed_extensions: tuple[str, ...] | None = None,
max_size: int | None = None,
) -> StoredFile
Validate and persist an uploaded file, returning its stored metadata.
The storage key is a random token, never the client-supplied filename —
that's what keeps this safe against path traversal and same-name
overwrites. The original filename is preserved in the returned
:class:StoredFile if you want to show/restore it.
Raises :class:~zeython.exceptions.ValidationException (422) if the
extension isn't in allowed_extensions, the file exceeds max_size,
or the file is empty.