Contrib integrations
Adapters mounting citry into web frameworks, and cache adapters for shared stores (the citry.contrib.<name> modules).
mount function
Mount citry_instance's routes into a FastAPI/Starlette app at prefix, and record the prefix on the instance.
A trailing / on prefix is dropped. When app lists its routes, as FastAPI and Starlette apps do, citry check and the editor can also ask it whether it serves each link path written in your templates.
For an app served below a sub-path, Citry's URLs (its scripts, styles, and Events endpoints) start with that sub-path. mount() adds a middleware to app that reads each request's ASGI root_path, which uvicorn --root-path or FastAPI(root_path=...) sets. It runs inside your own middleware, so one that sets root_path from a proxy header is seen. Outside a request, such as in a script or a test, the URLs use app.root_path, or the site root for a plain Starlette app. Call mount() before the server starts the app, for example at module level, not inside lifespan: Starlette accepts middleware only until then, so a later mount() skips it, and the URLs use app.root_path only. A page that one of your own middlewares renders runs outside Citry's middleware, so its URLs use app.root_path too.
Raises
ValueError- Ifprefixdoes not start with/, or if a route's full path (its parents' paths followed by its own) has two parameters side by side (such as{a}{b}) or uses one parameter name twice (seeURLRoute). The app is left unchanged.
mount function
Mount citry_instance's routes into a Flask app at prefix, and record the prefix on the instance.
A trailing / on prefix is dropped, so "/citry/" serves the same paths as "/citry". For a Flask app, citry check and the editor can also ask the app whether it serves each link path written in your templates.
Raises
ValueError- Ifprefixdoes not start with/, or is the root"/", or if a route's full path (its parents' paths followed by its own) has two parameters side by side (such as{a}{b}) or uses one parameter name twice (seeURLRoute). The app is left unchanged.
urlpatterns function
Citry.urls as Django URL patterns, for include()-ing.
Pass prefix (where you include the patterns, e.g. "/citry") to also record it on the instance, so URL building (fragment manifests, the runtime src) points at the right place; leaving it None means you call set_mounted_prefix yourself.
The generated views are synchronous, so every route handler must be a plain function; an async def handler raises TypeError here, pointing at the ASGI adapter as the fix. urlpatterns() also raises ValueError when a route's full path (its parents' paths followed by its own) has two parameters side by side (such as {a}{b}) or uses one parameter name twice.
Calling it also lets citry check and the editor ask the project whether its URL patterns or static files serve each link path written in your templates.
enable_hot_reload function
enable_hot_reload(citry_instance: Citry, mode: Literal['hot', 'restart'] = 'hot') -> Callable[..., bool | None]Pick up edits to component files in development, using Django's autoreloader.
Call it once at startup, for example from your AppConfig.ready(). When runserver starts its reloader, this asks the reloader to also watch every file under the engine's dirs (Citry(dirs=[...])). Django already watches Python files and its own template directories. When a watched file that a component has loaded changes, Citry clears that component's stored template, JS, and CSS (see invalidate_file()), so the next render reads the file again.
Other changed files are handled like this:
- A Python file goes to Django, which restarts the server.
- Any other file under the engine's
dirsneeds no action, because no component has read it yet, so the server keeps running. - Any other file outside the engine's
dirsgoes to Django's normal handling.
Calling it again for the same engine replaces the earlier call, so the last mode wins. Point the engine's dirs at your component folders, not the project root: the reloader checks every file under them, so a large folder makes it slow.
Example
::
# apps.py from django.apps import AppConfig from citry.contrib.django import enable_hot_reload from myapp import engine class MyAppConfig(AppConfig): name = "myapp" def ready(self): enable_hot_reload(engine)
Parameters
citry_instanceCitry- The engine whose components should pick up edits.modeLiteral['hot', 'restart']- What happens after Citry clears a changed component file."hot"(the default) keeps the server running, and the next render reads the new file."restart"restarts the server process, the same way Django restarts it after a Python edit.
Returns
Callable[..., bool | None]: The receiver connected to Django's ``file_changed`` signal.
Raises
ValueError- Whenmodeis not"hot"or"restart".
secret function
Django's SECRET_KEY and SECRET_KEY_FALLBACKS, for passing as Citry(secret=...).
Citry signs values with the engine-level secret setting. In a Django project the natural signing keys are the ones the project already manages, so pass them through::
from citry import Citry
from citry.contrib.django import secret
app = Citry(secret=secret())
When the project sets SECRET_KEY_FALLBACKS to rotate its key, Citry follows the rotation: SECRET_KEY signs new values, and a value signed with any of the fallback keys still verifies, so values Citry already put in visitors' pages keep verifying until you remove the old key. Empty and whitespace-only fallback entries are skipped.
The keys are read when this is called, so Django settings must be configured by then (in a normal Django startup they already are).
Returns
str | list[str]: ``SECRET_KEY`` as a plain string when ``SECRET_KEY_FALLBACKS`` is
Raises
RuntimeError- When Django settings are not configured; the message names the fix.django.core.exceptions.ImproperlyConfigured- When settings are configured but theSECRET_KEYitself is unusable (for example empty); Django's own message explains the problem.TypeError- WhenSECRET_KEY_FALLBACKSis a single string instead of a list, or holds an entry that is not a string.
DjangoCache class
Adapt a Django cache (django.core.cache.caches[...]) to citry's CitryCache protocol, so citry's stored scripts live in whatever cache backend the Django project already runs (Redis, Memcached, database, ...).
asgi_app function
Build the ASGI application serving citry_instance.urls.
The returned app handles lifespan events (so it also works served standalone) and routes each HTTP request to the matching citry handler, preferring the route's handler_async (its async version) when it has one. It builds a RouteRequest from the ASGI scope, awaits an async handler or runs a sync one in a worker thread, and sends the returned RouteResponse as ASGI messages.
Raises
ValueError- If a route's full path (its parents' paths followed by its own) has two parameters side by side (such as{a}{b}) or uses one parameter name twice. SeeURLRoute.
reload_lifespan function
reload_lifespan(engine: Citry, roots: Iterable[str | Path] | None = None, watcher: FileWatcher | None = None, on_reload: Callable[[set[Path], list[type[Component]]], None] | None = None) -> Callable[[Any], AbstractAsyncContextManager[None]]A Starlette/FastAPI lifespan that hot-reloads component files while the app runs.
Compose it into the app's root lifespan::
from contextlib import asynccontextmanager
from citry.contrib.asgi import reload_lifespan
watch_lifespan = reload_lifespan(citry_instance)
@asynccontextmanager
async def lifespan(app):
citry_instance.initialize()
async with watch_lifespan(app):
yield
app = FastAPI(lifespan=lifespan) # or Starlette(...)
It starts Citry's file watcher on startup and stops it on shutdown, so editing a component's template/JS/CSS shows up on the next render without restarting. It manages only the watcher and does not call Citry.initialize(); the root lifespan owns initialization. For development; in production simply do not add it. If you already have a lifespan, nest this one inside yours.
Parameters
engineCitry- The Citry instance whose component caches are reset when a watched file changes.rootsIterable[str | Path] | None- Directories to watch.Nonewatches the instance'sdirssetting.watcherFileWatcher | None- The file-watching backend.Noneuseswatchfilesif installed, elsewatchdog, else a polling watcher.on_reloadCallable[[set[Path], list[type[Component]]], None] | None- Called after each batch of changes with the changed paths and the component classes that were reset.
Returns
Callable[[Any], AbstractAsyncContextManager[None]]: A function that takes the app and returns the lifespan context
Raises
ValueError- On startup, if none of the roots exists. Missing directories are skipped.
wsgi_app function
wsgi_app(citry_instance: Citry) -> WSGIAppBuild the WSGI application serving citry_instance.urls.
The returned app routes each request to the matching citry handler. It builds a RouteRequest from the WSGI environ, calls the handler, and turns the returned RouteResponse into a WSGI response. Because WSGI is synchronous, a request that reaches an async def handler raises TypeError before the handler runs, and the message points to the ASGI adapter.
Raises
ValueError- If a route's full path (its parents' paths followed by its own) has two parameters side by side (such as{a}{b}) or uses one parameter name twice. SeeURLRoute.
RedisCache class
Adapt a Redis client (redis.Redis or compatible) to citry's CitryCache protocol. Values are stored as UTF-8 strings; ttl becomes the key's expiry (ex).
prefix is prepended to every key, for sharing a Redis database with other uses. (Citry's own keys already start with citry:.)
DiskCache class
Adapt a diskcache.Cache (or compatible) to citry's CitryCache protocol. The store is a directory on disk, shared by every worker process on the host.