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