Configuration

The asgiconfig dialect (the recipe you write), the grammar it speaks, and the read door the runtime reads it through.

AsgiConfigBuilder — the asgiconfig dialect: contrib/config + the server’s grammar.

The dialect is the contrib configuration builder (ConfigBuilder: the configuration root, the four-layer read contract, the XML render) composed with the grammar the server class declares (AsgiServer.grammar). A site subclasses it in a config.py and overrides main(self, root); the runtime reads the built tree through ConfigurationHandler and nothing else.

Recipes orchestrate in main and delegate each section to a method taking the PARENT node, so a section stays small enough to read at a glance:

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

class ServerConfiguration(AsgiConfigBuilder):
    def main(self, root):
        cfg = root.configuration()
        self.server_section(cfg)
        cfg.applications(default="shop").application(code="shop", app_class=Shop)

    def server_section(self, cfg):
        '''The listener and the session TTL.'''
        cfg.server(host="127.0.0.1", port=8000).session(ttl=3600)

BaseConfiguration ships the package’s OWN defaults in the same form — a recipe, not a dict of fallbacks. Every handler the server builds layers it under the site’s recipe, so a site inherits what it does not say; deviating means overriding one hook method.

class genro_asgi.config.builder.AsgiConfigBuilder(name=None)[source]

Bases: ConfigBuilder, AsgiServerGrammar

Configuration dialect of genro-asgi: contrib layout + AsgiServerGrammar.

Parameters:

name (str | None)

default_config: bool | str | Path | None = None

Where this recipe’s defaults layer comes from — DefaultConfig resolves it.

None (the default) takes the conventional <base_dir>/config.py when that file exists; False means no defaults layer at all; a path names the file, and a missing one is a ConfigError. The recipe governs its own inheritance — the server takes no kwarg for it.

class genro_asgi.config.builder.BaseConfiguration(name=None)[source]

Bases: AsgiConfigBuilder

The package’s shipped defaults, AS A RECIPE — the lowest layer of every site.

ConfigurationHandler layers it under the optional defaults recipe and the site’s own (DefaultConfig.parents_for()), so the defaults are executed through the grammar like any other recipe instead of being reproduced as constructor fallbacks. A site deviates by overriding ONE hook and nothing else — storage_key for the key material, storage_mounts for the layout, server_section for the listener:

from genro_bag.resolvers import EnvResolver

class ServerConfiguration(BaseConfiguration):
    storage_key = EnvResolver("GENRO_STORAGE_KEY")

    def storage_mounts(self, section):
        section.local(name="site", base_path="/srv/shop")
        section.s3(name="uploads", bucket="shop-media")

A recipe that subclasses AsgiConfigBuilder directly inherits the same defaults: the layering is the handler’s, not the class hierarchy’s.

Parameters:

name (str | None)

storage_key: str | BagResolver | None = None

At-rest key material of the storage section — a site sets it to a resolver.

main(root)[source]

The default document: the server section and the storage section.

Return type:

None

Parameters:

root (Any)

server_section(cfg)[source]

The server section, bare — the hook a machine or site recipe overrides.

It declares no value on purpose, and there is no signature default to inherit either: the element’s four parameters are all None, which the read stack reads as absent. The listener defaults stay where they live — in the constructor.

Return type:

None

Parameters:

cfg (Any)

storage_section(cfg)[source]

The storage section: genro-storage’s mount point plus the key material.

Return type:

None

Parameters:

cfg (Any)

storage_mounts(section)[source]

The default layout: one site: mount on the deployment directory.

The mount is DEFAULT_SITE_MOUNT written as a recipe line — the tag IS its protocol — so the layout the mixin builds without a recipe and the layout this recipe declares cannot drift apart.

The anchor is the cwd read WHEN THE RECIPE RUNS, which is boot: the same recipe follows whatever directory the deployment starts from. It is written absolute because genro-storage’s local backend rejects a relative base_path string outright.

Return type:

None

Parameters:

section (Any)

AsgiServerGrammar — the configuration grammar of AsgiServer.

The grammar the server class exposes as AsgiServer.grammar: one configuration root (the contrib ConfigBuilder element, OVERRIDDEN here with the full closed section list) whose sections describe the whole site. Every section is a singleton ([0:1]), so labels are clean and every path is stable and hand-writable: configuration.server, configuration.authentication.oidc.<code>, configuration.applications.<code>.

Authoring conventions inherited from contrib/config:

  • attributes are ANNOTATED, so their signature defaults reach the read stack of ConfigurationHandler (an unannotated parameter never enters call_args_validations);

  • an attribute whose value may come from outside (env, file, url) is annotated <type> | BagResolver and receives the resolver IN PLACE — there are no ^pointer strings in this dialect;

  • the recipe orchestrates in main and delegates each section to a method taking the PARENT node.

Sections:

  • server — the runtime options (host, port, external_url, max_threads, shutdown_timeout_seconds) plus the server-domain children session (the session TTL) and tasks (declared by TaskGrammar, the class that peels tasks=).

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

  • authentication — the whole identity surface: the bootstrap admin_password, the users/tokens store descriptors, the login lockout policy, the oidc provider collection and the credentials handed to AuthCore.

  • storage — the mount point of genro-storage’s own grammar: the mounts of the server’s StorageManager, plus the section’s storage_key.

  • applications — the app collection keyed by code; each entry MOUNTS the grammar its app_class carries.

  • databases — one descriptor per database handler.

  • plugins — the router plugins armed on every routed app.

  • openapi — the OpenAPI metadata (grammar only in core 1a).

The SPA pool is NOT a section of this dialect: a pool belongs to the application that owns it, so its words live in that application’s own grammar and its recipe is written under applications.<code>.orchestration.commander.

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

from myshop.app import Application as Shop

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

Bases: TaskGrammar

Configuration grammar of AsgiServer: the site layout, reading elsewhere.

Grammar only — the runtime reads the built tree through ConfigurationHandler, never through this class. Capability-owned companions are composed explicitly (TaskGrammar — the tasks child of server, owned by TaskMixin).

configuration = <genro_builders.builder._decorators._DeclarativeMarker object>
server = <genro_builders.builder._decorators._DeclarativeMarker object>
websocket = <genro_builders.builder._decorators._DeclarativeMarker object>
session = <genro_builders.builder._decorators._DeclarativeMarker object>
middleware = <genro_builders.builder._decorators._DeclarativeMarker object>
authentication = <genro_builders.builder._decorators._DeclarativeMarker object>
admin_password = <genro_builders.builder._decorators._DeclarativeMarker object>
users = <genro_builders.builder._decorators._DeclarativeMarker object>
tokens = <genro_builders.builder._decorators._DeclarativeMarker object>
login = <genro_builders.builder._decorators._DeclarativeMarker object>
oidc = <genro_builders.builder._decorators._DeclarativeMarker object>
provider = <genro_builders.builder._decorators._DeclarativeMarker object>
credentials = <genro_builders.builder._decorators._DeclarativeMarker object>
basic_user = <genro_builders.builder._decorators._DeclarativeMarker object>
bearer_token = <genro_builders.builder._decorators._DeclarativeMarker object>
jwt = <genro_builders.builder._decorators._DeclarativeMarker object>
storage = <genro_builders.builder._decorators._DeclarativeMarker object>
applications = <genro_builders.builder._decorators._DeclarativeMarker object>
application = <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>

ConfigurationHandler — the server’s read door on its configuration.

A contrib ConfigHandler subclass: it inherits the callable four-layer read stack (written value → signature default → call-site default= → noisy KeyError) and adds the section→kwargs mapping helpers AsgiServer.__init__ consumes. It NEVER builds a server: the server builds ITS OWN handler (AsgiServer(config=source)) and asks these helpers for its kwargs, so there is one direction of dependency and no materializer.

The helpers read the tree by two rules, and the grammar decides which applies:

  • a node with a CLOSED signature is read attribute by attribute THROUGH the handler itself, so the element’s signature defaults and any resolver sitting in an attribute are honored (server, provider, mount, …);

  • a node with OPEN **kwargs has no signature to consult, so its attributes are read in bulk through builder.runtime_values — resolvers resolved, everything else verbatim (application, plugin, database).

Section → constructor kwarg:

  • serverhost/port/external_url/max_threads/shutdown_timeout_seconds, its session child → session_ttl, its tasks child → tasks.

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

  • authenticationadmin_password/users/tokens (the store kwargs AuthMixin peels), auth (the AuthCore entries folded from credentials) and server_app (login + oidc, forwarded to the _server application).

  • storagestorage (genro-storage’s own list[dict] of mounts) and storage_key (the section’s at-rest key material).

  • applicationsapplications/default (each entry an (app_class, kwargs) pair the server instantiates).

  • databases → one descriptor per entry, registered by the server after the cooperative chain has run.

  • pluginsplugins ({code: bool | dict} switches).

  • openapi → no core-1a consumer; read and skipped.

  • orchestration → the SPA front’s whole orchestration subtree: its own three words, and under it commander — the vertex’s kwargs and one kwargs set per declared group (the two installation paths folded in, the child’s own keys gathered into its worker_kwargs).

exception genro_asgi.config.handler.ConfigError[source]

Bases: Exception

A configuration recipe names something the runtime cannot honor.

class genro_asgi.config.handler.ConfigurationHandler(source, parents=None)[source]

Bases: ConfigHandler

Read door over an asgiconfig tree, plus the section→kwargs mapping.

Parameters:
  • source (str | Path | type | BuilderBase)

  • parents (Sequence[str | Path | type | BuilderBase] | None)

server_kwargs()[source]

The server section as server kwargs, its children lifted.

session becomes session_ttl, tasks becomes the tasks tuning dict and websocket the websocket options: all three are server-domain (sessions, the task backbone and the sockets live on the server), so their values lift to the kwargs the owning mixins peel while the config keeps them under server where they belong. The origins of a handshake are written as one comma-separated string in a recipe and reach the server as the list it reads.

Return type:

dict[str, Any]

middleware_config()[source]

The middleware switches, or None when the section is absent (the composition’s own defaults then apply).

Return type:

dict[str, Any] | None

identity_kwargs()[source]

The identity STORE kwargs of authentication (AuthMixin peels them).

admin_password is the admin_password node’s VALUE, which a resolver must supply — the grammar rejects a literal at the recipe line (secrets stay out of recipes). Resolving empty is a boot error (the recipe promised a secret that does not exist — an empty bootstrap password would arm a passwordless SUPERADMIN), and so is resolving to a non-string. users/tokens are {mount, prefix} descriptors.

Return type:

dict[str, Any]

admin_password(node)[source]

The bootstrap password carried by node, resolved to a non-empty string.

The grammar already rejects a literal at the recipe line (node_value: BagResolver); here we validate what the resolver actually DELIVERED at boot.

Return type:

str

Parameters:

node (Any)

auth_entries()[source]

The credentials children folded into the AuthCore sections.

basic_user entries are keyed by username and bearer_token entries by identity — the keys AuthCore reads back as the authenticated identity — while jwt entries stay an ORDERED list (the first verifier that verifies wins). None when nothing is configured: the server then arms no header backend.

Return type:

dict[str, Any] | None

server_app_kwargs()[source]

The LOGIN surface of authentication → the _server app’s kwargs.

login is the lockout policy and oidc the providers keyed by code. These values belong to the application that peels them, so they travel as ONE server kwarg (server_app) forwarded at mount time instead of being lifted onto the server itself.

Return type:

dict[str, Any]

oidc_providers()[source]

The oidc providers as {code: attrs}, defaults applied.

scopes and identity_claim come from the element’s signature, so every provider carries them whether the recipe wrote them or not; tags defaults to the empty list here (a mutable signature default is never declared).

Return type:

dict[str, dict[str, Any]]

storage_config()[source]

The storage section as (mounts, storage_key), or None when it is absent (the composition builds its default manager).

The subtree is written in genro-storage’s grammar, so it is flattened GENERICALLY into that library’s list[dict]: the tag IS the protocol and every attribute rides through as-is (name among them — the envelope is transparent to containment, so the children carry auto labels and their key lives in the attribute the foreign grammar declares). This dialect knows no storage vocabulary to translate.

A section carrying only its storage_key and no mount is legitimate — “the default layout, plus this key” — so it yields an EMPTY mount list rather than an error; the composition reads that as “use the default site: mount”. With BaseConfiguration layered underneath the merged tree normally carries the site mount anyway, so this is the shape a handler built without parents produces.

Return type:

tuple[list[dict[str, Any]], str | None] | None

plugins_config()[source]

The plugins switches as {code: bool | dict}, or None when the section is absent (the composition arms no extra plugin).

A plugin maps to False when enabled is explicitly false, to its remaining options when it carries any, else to True.

Return type:

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

applications()[source]

The declared applications as (app_class, kwargs) pairs, plus default.

Every attribute of the envelope except app_class is a constructor kwarg of the application — code and mount included, since the app owns their resolution. The mounted subtree is NOT passed: an application reads its own configuration back through the handler (applications.<code>.<path>), it never receives a slice of the tree.

Return type:

tuple[list[tuple[type, dict[str, Any]]], str | None]

databases()[source]

The databases descriptors as {code, db_class, db_handler_class, params}.

db_handler_class is None when the recipe omits it (the server substitutes its default) and params are the remaining connection kwargs handed to db_class(**params).

Return type:

list[dict[str, Any]]

orchestration_kwargs(code)[source]

One application’s orchestration node, or None when it has none.

Parameters:

code (str) – the application whose orchestration this is — the words live under applications.<code>.orchestration.

Return type:

dict[str, Any] | None

Returns:

profiles_path, profile_name and control_enabled, the three the recipe actually wrote, or None when the node is absent — which is a front that declares no pool at all.

commander_kwargs(code)[source]

The pool of one application as its vertex’s own constructor kwargs.

Parameters:

code (str) – the application whose pool this is — a pool belongs to the front that owns it, so the words live under applications.<code>.orchestration.commander.

Return type:

dict[str, Any] | None

Returns:

The vertex’s kwargs, or None when the node is absent — an orchestration node with no commander under it, which the front refuses.

instance_dir is NOT among them: the sockets are the workers’ business, so that path is folded into every group instead (group_kwargs). The group ELECTED to receive a newcomer is declared one level down, on the collection, and is folded in here because the vertex is what reads it. What the recipe leaves out is left out, and the vertex’s own default answers.

group_kwargs(code)[source]

One application’s groups as {name: kwargs}, one GroupHandler each.

Parameters:

code (str) – the application whose pool these groups belong to.

Return type:

dict[str, dict[str, Any]]

The two paths of the installation live on commander and are folded in here, because a group is what builds the workers that need them. The one key the CHILD reads travels in its own worker_kwargs: the group’s name, which stamps every item it writes. So a recipe writes each policy once, on the rung it belongs to, and the child is handed what is his.

node(path)[source]

The node at path (relative to the root element), or None.

Return type:

Any

Parameters:

path (str)

closed_attrs(path, *names)[source]

Read names at path through the read stack, skipping the absent.

One four-layer read per attribute, so a resolver sitting in an attribute resolves and the element’s signature defaults apply. None means “not configured and no default” and is left out — the consumer’s own default then applies.

Return type:

dict[str, Any]

Parameters:
open_attrs(node)[source]

Every attribute node carries, resolvers resolved.

The read for elements whose signature is OPEN (**kwargs): there is no declared attribute list to walk and no signature default to consult, so the node’s own attributes are the whole truth.

Return type:

dict[str, Any]

Parameters:

node (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