Configuration

The config builder, the configuration handler, and the config-as-data machinery (elements, projection).

AsgiConfigBuilder — the asgiconfig builder dialect (D15).

The user subclasses AsgiConfigBuilder in a config.py and overrides main(self, root) (the recipe). ConfigurationHandler mounts the builder, runs the recipe and MATERIALIZES an AsgiServer by walking the built tree — unlike the markup dialects (HTML, SVG) there is no renderer: the configuration is read back directly from the SourceBag (node.node_tag / node.fixed_attr_items()).

Example config.py:

from genro_asgi.config import AsgiConfigBuilder
from myshop.app import Application as Shop

class ServerConfiguration(AsgiConfigBuilder):
    def main(self, root):
        root.server(host="127.0.0.1", port=8000)
        root.middleware(cors=True)
        apps = root.applications(default="shop")
        apps.application(code="shop", app_class=Shop)
class genro_asgi.config.builder.AsgiConfigBuilder(name=None)[source]

Bases: BuilderBase, AsgiConfigElements

Server-configuration dialect: grammar from AsgiConfigElements.

Carries no renderer — the configuration is walked back out of the built source tree by ConfigurationHandler.materialize.

Parameters:

name (str | None)

ConfigurationHandler — owns the config builder and MATERIALIZES a server.

DIVERGENCE from the old repo (recorded per the phase plan): the old AsgiConfigRenderer mutated a LIVE server through apply_configuration; this handler MATERIALIZES a fresh AsgiServer instead — materialize() walks the built source tree, turns the sections into constructor kwargs riding the D16 cooperative chain, instantiates AsgiServer(**kwargs) and mounts the secondary apps. No post-hoc mutation of server state.

It subclasses genro_buildersBuilderHandler only to run the recipe (add_builder → the builder’s create); the configuration is read back by walking the SourceBag directly (node.node_tag / node.fixed_attr_items()), never through a renderer.

Section → constructor kwarg mapping (core 1a):

  • serverhost/port (the AsgiServer.serve defaults), external_url (the public base address, required by a configured OIDC provider) plus max_threads (the WorkPool size, peeled by BaseServer) and storage_key (the StorageMixin encryption key); its session child → session_ttl (server-domain).

  • middlewaremiddleware= ({name: bool | dict} switches).

  • authauth= (the AuthCore config, handed verbatim).

  • storagestorage= ({code: {path, encrypted}} mounts for the StorageMixin); visible to every role.

  • applicationsprimary= (the default app, mount /) plus the secondary mounts (each mounted after construction; mount defaults to code).

  • databases → one db_handler_class(db_class(**params)) per entry, registered on the server by code (core 1b).

  • pluginsplugins= ({code: bool | dict} switches for the PluginMixin); visible to every role.

  • openapi/nested groups/nested configuration → read and SKIPPED with a debug log (valid config for other roles/macros — configuration is the app’s own opaque subtree, consumed at runtime — not an error).

App config-classes: the constructor takes the site recipe FIRST plus any app-config builders (ConfigurationHandler(site, MyServerAppConfig(...))). Each extra builder is claimed by the application it configures via class identity — today only _server (ServerApplication.config_class); a builder nobody claims is a boot error. The claimed config’s values (admin_password/users/tokens) LIFT to the server constructor kwargs (root role only); without one the base default applies (empty recipe, today’s bare ServerApplication()).

materialize(role=..., app=...) computes the role’s Projection of the built tree (D15) and materializes THAT slice: root sees every section above; the hosted roles (worker/batch, app=<code> required) see only their application (as primary) plus databases and storage — never the public middleware, never auth or sessions, never the public listener address.

class genro_asgi.config.handler.ConfigurationHandler(builder, *app_configs)[source]

Bases: BuilderHandler

Owns the AsgiConfigBuilder recipe and materializes an AsgiServer.

Parameters:
  • builder (Any)

  • app_configs (Any)

property builder: Any

The mounted configuration builder (its recipe already run).

property server_app_config: Any

The _server app’s config builder (the claimed one or the default).

property logger: Logger

This handler’s instance logger.

materialize(role='root', app=None)[source]

Build a fresh AsgiServer from the (config, role) projection (D15).

Computes the role’s Projection of the built section tree, maps each visible section to its constructor kwarg, instantiates AsgiServer and mounts the secondary apps. The hosted roles (worker/batch) name their application with app=<code>. Sections with no core-1a applier are read and skipped with a debug log.

Return type:

AsgiServer

Parameters:

Configurable classes — per-class configuration grammars (extraction-ready).

The configuration grammar follows the class hierarchy: every runtime class can declare its own grammar as a companion mixin of @element markers, linked through the config_grammar class attribute:

class VehicleElements:
    @element(sub_tags="")
    def wheels(self) -> None:
        '''Wheel options.'''

class Vehicle:
    config_grammar = VehicleElements

Companions mirror the runtime hierarchy with plain Python inheritance (CarElements(VehicleElements) as Car(Vehicle)), so grammars compose along the chain exactly like D16 cooperative constructors — an element declared by class X materializes into the kwarg class X peels. The site recipe composes companions EXPLICITLY (class Site(BuilderBase, Car.config_grammar)): no auto-discovery, no global registry. genro-builders collects the markers from mixin bases without removing them (_pop_decorated_methods), so a companion stays introspectable after any number of recipes consumed it.

The module offers three operations over that convention:

  • declared_elements() — which tags a companion chain declares (MRO scan of the _decorator markers).

  • element_kwargs() — the materializer: a node’s attributes become constructor kwargs, each child element becomes a dict-valued kwarg keyed by its tag (users(mount=...) -> users={"mount": ...}). STRICT: a child not declared by the owner’s grammar chain, a duplicate child, or a child carrying children of its own raise ConfigError — never a silent skip (D16 symmetry).

  • resolve_pointers() — resolve a node’s ^ pointers through the builder’s runtime_values; a configured pointer that resolves empty (None or “”) raises ConfigError (a secret the recipe promised MUST exist — no silent degradation).

EXTRACTION BOUNDARY: this module imports nothing from genro_asgi and nothing ASGI — it is duck-typed over genro-builders’ public seams (the _decorator marker attribute, the source-node API, runtime_values) and moves as-is to genro-builders or a standalone package, tests included.

exception genro_asgi.config.configurable.ConfigError[source]

Bases: Exception

A configuration recipe names something its grammar chain cannot honor.

genro_asgi.config.configurable.declared_elements(grammar)[source]

Return the tags declared by grammar’s companion chain.

Walks the companion’s MRO collecting every attribute that carries a _decorator marker; @abstract declarations (inheritance-only) and @container methods (source generators, not tags) are skipped — the same filter genro-builders applies when it builds a class schema.

Return type:

set[str]

Parameters:

grammar (type)

genro_asgi.config.configurable.element_kwargs(node, owner_class, exclude=())[source]

Materialize node into constructor kwargs for owner_class.

The node’s attributes become kwargs (minus exclude, the names the caller consumes itself); each child element becomes a dict-valued kwarg keyed by its tag. Strict-unknown: a child whose tag is not declared by owner_class.config_grammar’s chain (or any child when the owner has no config_grammar) raises ConfigError, as do a duplicate child tag and a child carrying children of its own (the materializer maps ONE level: child -> dict kwarg).

Return type:

dict[str, Any]

Parameters:
genro_asgi.config.configurable.resolve_pointers(builder, node)[source]

Resolve node’s ^ pointers via builder.runtime_values, strictly.

Returns the (runtime_value, runtime_attrs) pair unchanged, after enforcing the boot rule: a configured ^ pointer (attribute or node value) that resolves empty — None or “” — raises ConfigError naming the subject and the pointer. Non-pointer attributes pass through untouched.

Return type:

tuple[Any, dict[str, Any]]

Parameters:

AsgiConfigElements — grammar for the asgiconfig dialect (D15).

The config recipe describes the WHOLE site (D15), even where core 1a does not materialize every section. Each @element is a top-level section built directly on the recipe root (root.server(...), root.applications(...)); there is no enclosing config node. A section’s kwargs become the node’s attributes (read back by ConfigurationHandler.materialize via node.node_tag / node.fixed_attr_items()).

Sections and their 1a fate:

  • server — runtime options (host, port, external_url, max_threads, storage_key). host/port feed AsgiServer.serve; external_url is the server’s public base address (the OIDC redirect_uri prefix); max_threads sizes the server’s thread pool (peeled by BaseServer, handed to WorkPool); storage_key installs at-rest encryption on the server’s storage (core 1b).

  • middleware — one {name: bool | dict} switch per middleware.

  • storage — mounts of the server’s LocalStorage (core 1b); visible to every role (survival infrastructure).

  • auth — the credential config (basic/bearer/jwt kwargs) handed verbatim to AuthCore via the auth= server kwarg.

  • applications/application — the app collection keyed by code; the optional default names the primary (served on /). Each app derives its mount from code unless it declares mount.

  • groups/group — grammar only (orchestration package).

  • databases/database — grammar only (core 1b).

  • plugins/plugin — router plugins armed onto every routed app (PluginMixin); visible to every role (a worker needs the same router behavior).

  • openapi — grammar only (core 1c).

A recipe subclasses AsgiConfigBuilder and overrides main(self, root); application classes are imported by the recipe and passed as objects:

from myshop.app import Application as Shop

def main(self, root):
    root.server(host="127.0.0.1", port=8000)
    root.middleware(cors=True)
    apps = root.applications(default="shop")
    apps.application(code="shop", app_class=Shop)
class genro_asgi.config.elements.AsgiConfigElements[source]

Bases: TaskConfigElements

Element mixin for the asgiconfig dialect. Grammar only.

The sections are top-level elements built directly on the recipe root; a section’s kwargs are stored as the node’s attributes and read back at materialization time. Capability-owned companions are composed explicitly (TaskConfigElements — the tasks child of server, owned by TaskMixin).

server = <genro_builders.builder._decorators._DeclarativeMarker object>
session = <genro_builders.builder._decorators._DeclarativeMarker object>
middleware = <genro_builders.builder._decorators._DeclarativeMarker object>
auth = <genro_builders.builder._decorators._DeclarativeMarker object>
storage = <genro_builders.builder._decorators._DeclarativeMarker object>
mount = <genro_builders.builder._decorators._DeclarativeMarker object>
applications = <genro_builders.builder._decorators._DeclarativeMarker object>
application = <genro_builders.builder._decorators._DeclarativeMarker object>
configuration = <genro_builders.builder._decorators._DeclarativeMarker object>
groups = <genro_builders.builder._decorators._DeclarativeMarker object>
group = <genro_builders.builder._decorators._DeclarativeMarker object>
databases = <genro_builders.builder._decorators._DeclarativeMarker object>
database = <genro_builders.builder._decorators._DeclarativeMarker object>
plugins = <genro_builders.builder._decorators._DeclarativeMarker object>
plugin = <genro_builders.builder._decorators._DeclarativeMarker object>
openapi = <genro_builders.builder._decorators._DeclarativeMarker object>

Projection — the per-role slice of one whole-site config (D15).

One config describes the ENTIRE site; every process is born with (config, role) and materializes its own slice. Projection IS that slice: a slotted object computed from the built SourceBag tree plus the role (plus app=<code> for the hosted roles), consumed by ConfigurationHandler.materialize. The projection is PER-SECTION:

  • root — the public server: every section (server, middleware, auth, storage, applications, databases, plugins, openapi); every application mounted, the default one primary.

  • worker — the hosted process of ONE application (app=<code> required): sees only that application (as primary, no secondaries) plus databases, storage and plugins (D15: the transversal pieces it needs — storage is survival infrastructure, plugins are router behavior) — never the public middleware, never auth or sessions, never the public listener address.

  • batch — the same section cut as worker with no HTTP-facing expectations (still an AsgiServer composition in 1a; what 1a fixes is WHICH SECTIONS each role sees — the orchestration semantics arrive with the orchestration package).

The role → visible-sections rules table lives ON THE INSTANCE — nothing at module level. Errors are explicit: an unknown role raises ValueError naming it; a hosted role without app= raises TypeError; root with app= raises TypeError (it mounts every application); a hosted role naming an undeclared application raises ValueError.

class genro_asgi.config.projection.Projection(source, role, app=None)[source]

Bases: object

The (role, app) slice of a built config tree (D15, per-section).

Parameters:
  • source (Any)

  • role (str)

  • app (str | None)

property role: str

The declared role this slice was computed for.

property app: str | None

The hosted application code (None for the root role).

property visible_sections: frozenset[str]

The section tags this role sees (the D15 per-section rules table).

section(name)[source]

The named section node, or None when absent or invisible to the role.

Return type:

Any

Parameters:

name (str)

server_attrs()[source]

The server section attributes of this slice (empty for hosted roles).

The public listener address (host/port) belongs to the root process; a hosted worker/batch process never inherits it. Only the node’s own attributes — the session child is read through its own seam.

Return type:

dict[str, Any]

session_node()[source]

The server section’s session child node, or None.

Server-domain: absent for hosted roles (they never see server).

Return type:

Any

tasks_node()[source]

The server section’s tasks child node, or None.

Server-domain: absent for hosted roles (they never see server).

Return type:

Any

middleware_config()[source]

The middleware switches of this slice.

root: the middleware section attributes, or None when the section is absent (the composition’s own defaults apply). Hosted roles: every default-armed middleware explicitly OFF — errors (middleware_default ON) and the auth/session switches the mixins arm via setdefault — D15’s “never the public middleware”; an explicit False always wins over the arming.

Return type:

dict[str, Any] | None

auth_config()[source]

The auth section attributes, or None when absent.

Hosted roles never see the auth section, so this degrades to None for them with no special casing.

Return type:

dict[str, Any] | None

storage_config()[source]

The storage mounts of this slice as {code: {path, encrypted}}.

None when the section is absent (the composition builds a default LocalStorage). Visible to every role — storage is D15 survival infrastructure, not root-only like middleware/auth.

Return type:

dict[str, Any] | None

databases_config()[source]

The databases descriptors of this slice as {code: {db_class, db_handler_class, params}}.

Empty dict when the section is absent or invisible to the role. db_handler_class is None when the recipe omits it (the materializer’s default applies); params are the remaining connection kwargs handed to db_class(**params). Visible to root, worker and batch (D15 transversal): 1b materializes the whole section for every role that sees it — the per-mount slice arrives with orchestration.

Return type:

dict[str, dict[str, Any]]

plugins_config()[source]

The plugins switches of this slice as {code: bool | dict}.

None when the section is absent (the composition arms no extra plugins). Each plugin child maps to False when enabled is explicitly false, to its remaining options dict when it carries any, else to True. Visible to every role — plugins are D15 transversal router behavior, like storage and databases.

Return type:

dict[str, bool | dict[str, Any]] | None

applications()[source]

The role’s application cut: (primary_node, secondary_nodes).

root: the default app (or the lone app when default is omitted) is the primary, every other app a secondary. Hosted roles: ONLY the app= application, as primary with no secondaries.

Return type:

tuple[Any, list[Any]]

Database handler

Database handler — the core’s minimal contract for a mounted database.

A database declared in the config names a db_class (the imported class that builds the real db from the connection parameters) and, optionally, a db_handler_class (default AsgiDbHandlerBase). At mount time the server builds db_handler_class(db_class(**params)) and registers the handler.

The handler is what lives in the registry and what request.db returns. It proxies every attribute to the wrapped db via __getattr__ (so the db’s own interface — execute and the rest — stays transparent), while owning the one method the core itself calls: closeConnection (registered as a request cleanup). Concrete db classes and custom handlers live outside the core; the core only defines this contract.

class genro_asgi.db.AsgiDbHandlerBase(db)[source]

Bases: object

Wraps a database object: owns closeConnection, proxies the rest.

Subclass to customise lifecycle (e.g. a legacy backend); the default proxies every non-underscore attribute to the wrapped db.

Parameters:

db (Any)

closeConnection()[source]

Close the wrapped db’s connection if it exposes closeConnection.

Return type:

None