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,AsgiConfigElementsServer-configuration dialect: grammar from
AsgiConfigElements.Carries no renderer — the configuration is walked back out of the built
sourcetree byConfigurationHandler.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_builders’ BuilderHandler 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):
server→host/port(theAsgiServer.servedefaults),external_url(the public base address, required by a configured OIDC provider) plusmax_threads(theWorkPoolsize, peeled byBaseServer) andstorage_key(theStorageMixinencryption key); itssessionchild →session_ttl(server-domain).middleware→middleware=({name: bool | dict} switches).auth→auth=(theAuthCoreconfig, handed verbatim).storage→storage=({code: {path, encrypted}} mounts for theStorageMixin); visible to every role.applications→primary=(thedefaultapp, mount/) plus the secondary mounts (each mounted after construction; mount defaults tocode).databases→ onedb_handler_class(db_class(**params))per entry, registered on the server bycode(core 1b).plugins→plugins=({code: bool | dict} switches for thePluginMixin); visible to every role.openapi/nestedgroups/nestedconfiguration→ read and SKIPPED with a debug log (valid config for other roles/macros —configurationis 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:
BuilderHandlerOwns the
AsgiConfigBuilderrecipe and materializes anAsgiServer.- Parameters:
builder (Any)
app_configs (Any)
- materialize(role='root', app=None)[source]
Build a fresh
AsgiServerfrom the (config, role) projection (D15).Computes the role’s
Projectionof the built section tree, maps each visible section to its constructor kwarg, instantiatesAsgiServerand mounts the secondary apps. The hosted roles (worker/batch) name their application withapp=<code>. Sections with no core-1a applier are read and skipped with a debug log.- Return type:
- 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_decoratormarkers).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 raiseConfigError— never a silent skip (D16 symmetry).resolve_pointers()— resolve a node’s^pointers through the builder’sruntime_values; a configured pointer that resolves empty (None or “”) raisesConfigError(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:
ExceptionA 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
_decoratormarker;@abstractdeclarations (inheritance-only) and@containermethods (source generators, not tags) are skipped — the same filter genro-builders applies when it builds a class schema.
- genro_asgi.config.configurable.element_kwargs(node, owner_class, exclude=())[source]
Materialize
nodeinto constructor kwargs forowner_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 byowner_class.config_grammar’s chain (or any child when the owner has noconfig_grammar) raisesConfigError, as do a duplicate child tag and a child carrying children of its own (the materializer maps ONE level: child -> dict kwarg).
- genro_asgi.config.configurable.resolve_pointers(builder, node)[source]
Resolve
node’s^pointers viabuilder.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 “” — raisesConfigErrornaming the subject and the pointer. Non-pointer attributes pass through untouched.
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/portfeedAsgiServer.serve;external_urlis the server’s public base address (the OIDCredirect_uriprefix);max_threadssizes the server’s thread pool (peeled byBaseServer, handed toWorkPool);storage_keyinstalls at-rest encryption on the server’s storage (core 1b).middleware— one{name: bool | dict}switch per middleware.storage— mounts of the server’sLocalStorage(core 1b); visible to every role (survival infrastructure).auth— the credential config (basic/bearer/jwtkwargs) handed verbatim toAuthCorevia theauth=server kwarg.applications/application— the app collection keyed bycode; the optionaldefaultnames the primary (served on/). Each app derives its mount fromcodeunless it declaresmount.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:
TaskConfigElementsElement mixin for the
asgiconfigdialect. 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— thetaskschild ofserver, owned byTaskMixin).- 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, thedefaultone primary.worker— the hosted process of ONE application (app=<code>required): sees only that application (as primary, no secondaries) plusdatabases,storageandplugins(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 asworkerwith no HTTP-facing expectations (still anAsgiServercomposition 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:
objectThe (role, app) slice of a built config tree (D15, per-section).
- property visible_sections: frozenset[str]
The section tags this role sees (the D15 per-section rules table).
- server_attrs()[source]
The
serversection 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 — thesessionchild is read through its own seam.
- session_node()[source]
The
serversection’ssessionchild node, orNone.Server-domain: absent for hosted roles (they never see
server).- Return type:
- tasks_node()[source]
The
serversection’staskschild node, orNone.Server-domain: absent for hosted roles (they never see
server).- Return type:
- middleware_config()[source]
The middleware switches of this slice.
root: themiddlewaresection attributes, orNonewhen the section is absent (the composition’s own defaults apply). Hosted roles: every default-armed middleware explicitly OFF —errors(middleware_defaultON) and theauth/sessionswitches the mixins arm viasetdefault— D15’s “never the public middleware”; an explicitFalsealways wins over the arming.
- auth_config()[source]
The
authsection attributes, orNonewhen absent.Hosted roles never see the
authsection, so this degrades toNonefor them with no special casing.
- storage_config()[source]
The
storagemounts of this slice as{code: {path, encrypted}}.Nonewhen the section is absent (the composition builds a defaultLocalStorage). Visible to every role — storage is D15 survival infrastructure, not root-only like middleware/auth.
- databases_config()[source]
The
databasesdescriptors 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_classisNonewhen the recipe omits it (the materializer’s default applies);paramsare the remaining connection kwargs handed todb_class(**params). Visible toroot,workerandbatch(D15 transversal): 1b materializes the whole section for every role that sees it — the per-mount slice arrives with orchestration.
- plugins_config()[source]
The
pluginsswitches of this slice as{code: bool | dict}.Nonewhen the section is absent (the composition arms no extra plugins). Eachpluginchild maps toFalsewhenenabledis explicitly false, to its remaining options dict when it carries any, else toTrue. Visible to every role — plugins are D15 transversal router behavior, like storage and databases.
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.