Source code for genro_asgi.config.builder

# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""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.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from genro_bag import BagResolver
from genro_builders.contrib.config import ConfigBuilder
from genro_storage import StorageManager

from ..storage_mixin import DEFAULT_SITE_MOUNT
from .elements import AsgiServerGrammar

__all__ = ["AsgiConfigBuilder", "BaseConfiguration"]


[docs] class AsgiConfigBuilder(ConfigBuilder, AsgiServerGrammar): """Configuration dialect of genro-asgi: contrib layout + ``AsgiServerGrammar``.""" _name = "asgiconfig" 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. """
[docs] class BaseConfiguration(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. """ storage_key: str | BagResolver | None = None """At-rest key material of the storage section — a site sets it to a resolver."""
[docs] def main(self, root: Any) -> None: """The default document: the server section and the storage section.""" cfg = root.configuration() self.server_section(cfg) self.storage_section(cfg)
[docs] def server_section(self, cfg: Any) -> None: """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. """ cfg.server()
[docs] def storage_section(self, cfg: Any) -> None: """The ``storage`` section: genro-storage's mount point plus the key material.""" self.storage_mounts(cfg.storage(app=StorageManager, storage_key=self.storage_key))
[docs] def storage_mounts(self, section: Any) -> None: """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. """ section.local(name=DEFAULT_SITE_MOUNT["name"], base_path=str(Path.cwd()))