Skip to content

src/genro_asgi/config/builder.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """AsgiConfigBuilder — the ``asgiconfig`` dialect: contrib/config + the server's grammar.16 17 The dialect is the contrib configuration builder (``ConfigBuilder``: the18 ``configuration`` root, the four-layer read contract, the XML render) composed19 with the grammar the server class declares (``AsgiServer.grammar``). A site20 subclasses it in a ``config.py`` and overrides ``main(self, root)``; the runtime21 reads the built tree through ``ConfigurationHandler`` and nothing else.22 23 Recipes orchestrate in ``main`` and delegate each section to a method taking the24 PARENT node, so a section stays small enough to read at a glance::25 26     from genro_asgi.config import AsgiConfigBuilder27     from myshop.app import Application as Shop28 29     class ServerConfiguration(AsgiConfigBuilder):30         def main(self, root):31             cfg = root.configuration()32             self.server_section(cfg)33             cfg.applications(default="shop").application(code="shop", app_class=Shop)34 35         def server_section(self, cfg):36             '''The listener and the session TTL.'''37             cfg.server(host="127.0.0.1", port=8000).session(ttl=3600)38 39 ``BaseConfiguration`` ships the package's OWN defaults in the same form — a40 recipe, not a dict of fallbacks. Every handler the server builds layers it41 under the site's recipe, so a site inherits what it does not say; deviating42 means overriding one hook method.43 """44 45 from __future__ import annotations46 47 from pathlib import Path48 from typing import Any49 50 from genro_bag import BagResolver51 from genro_builders.contrib.config import ConfigBuilder52 from genro_storage import StorageManager53 54 from ..storage_mixin import DEFAULT_SITE_MOUNT55 from .elements import AsgiServerGrammar56 57 __all__ = ["AsgiConfigBuilder", "BaseConfiguration"]58 59 60 class AsgiConfigBuilder(ConfigBuilder, AsgiServerGrammar):61     """Configuration dialect of genro-asgi: contrib layout + ``AsgiServerGrammar``."""62 63     _name = "asgiconfig"64 65     default_config: bool | str | Path | None = None66     """Where this recipe's defaults layer comes from — ``DefaultConfig`` resolves it.67 68     ``None`` (the default) takes the conventional ``<base_dir>/config.py`` when69     that file exists; ``False`` means no defaults layer at all; a path names the70     file, and a missing one is a ``ConfigError``. The recipe governs its own71     inheritance — the server takes no kwarg for it.72     """73 74 75 class BaseConfiguration(AsgiConfigBuilder):76     """The package's shipped defaults, AS A RECIPE — the lowest layer of every site.77 78     ``ConfigurationHandler`` layers it under the optional defaults recipe and the79     site's own (``DefaultConfig.parents_for()``), so the defaults are *executed*80     through the grammar like any other recipe instead of being reproduced as81     constructor fallbacks. A site deviates by overriding ONE hook and nothing82     else — ``storage_key`` for the key material, ``storage_mounts`` for the83     layout, ``server_section`` for the listener::84 85         from genro_bag.resolvers import EnvResolver86 87         class ServerConfiguration(BaseConfiguration):88             storage_key = EnvResolver("GENRO_STORAGE_KEY")89 90             def storage_mounts(self, section):91                 section.local(name="site", base_path="/srv/shop")92                 section.s3(name="uploads", bucket="shop-media")93 94     A recipe that subclasses ``AsgiConfigBuilder`` directly inherits the same95     defaults: the layering is the handler's, not the class hierarchy's.96     """97 98     storage_key: str | BagResolver | None = None99     """At-rest key material of the storage section — a site sets it to a resolver."""100 101     def main(self, root: Any) -> None:102         """The default document: the server section and the storage section."""103         cfg = root.configuration()104         self.server_section(cfg)105         self.storage_section(cfg)106 107     def server_section(self, cfg: Any) -> None:108         """The ``server`` section, bare — the hook a machine or site recipe overrides.109 110         It declares no value on purpose, and there is no signature default to111         inherit either: the element's four parameters are all ``None``, which the112         read stack reads as absent. The listener defaults stay where they live —113         in the constructor.114         """115         cfg.server()116 117     def storage_section(self, cfg: Any) -> None:118         """The ``storage`` section: genro-storage's mount point plus the key material."""119         self.storage_mounts(cfg.storage(app=StorageManager, storage_key=self.storage_key))120 121     def storage_mounts(self, section: Any) -> None:122         """The default layout: one ``site:`` mount on the deployment directory.123 124         The mount is ``DEFAULT_SITE_MOUNT`` written as a recipe line — the tag IS125         its ``protocol`` — so the layout the mixin builds without a recipe and the126         layout this recipe declares cannot drift apart.127 128         The anchor is the cwd read WHEN THE RECIPE RUNS, which is boot: the same129         recipe follows whatever directory the deployment starts from. It is130         written absolute because genro-storage's local backend rejects a131         relative ``base_path`` string outright.132         """133         section.local(name=DEFAULT_SITE_MOUNT["name"], base_path=str(Path.cwd()))