Source code for genro_asgi.server.server_config

# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0

"""Server configuration - handles all config loading and app instantiation."""

from __future__ import annotations

from pathlib import Path
from typing import Any

from genro_toolbox import SmartOptions  # type: ignore[import-untyped]

__all__ = ["ServerConfig"]

DEFAULTS = {"host": "127.0.0.1", "port": 8000, "reload": False}


def _server_opts_spec(
    server_dir: str,
    host: str,
    port: int,
    reload: bool,
    config: str,
) -> None:
    """Reference function for SmartOptions type extraction (no defaults)."""


[docs] class ServerConfig: """Handles server configuration loading and app instantiation.""" __slots__ = ("_opts", "_openapi") def __init__( self, server_dir: str | Path | None = None, host: str | None = None, port: int | None = None, reload: bool | None = None, argv: list[str] | None = None, ) -> None: self._opts = self._build_config( server_dir=server_dir, host=host, port=port, reload=reload, argv=argv or [], ) # Convert openapi to plain dict once (SmartOptions auto-converts on assignment) openapi_cfg = self._opts["openapi"] if openapi_cfg and hasattr(openapi_cfg, "as_dict"): self._openapi: dict[str, Any] = openapi_cfg.as_dict() elif openapi_cfg: self._openapi = dict(openapi_cfg) else: self._openapi = {"title": "genro-asgi API", "version": "0.1.0"} def _build_config( self, server_dir: str | Path | None, host: str | None, port: int | None, reload: bool | None, argv: list[str], ) -> SmartOptions: """Build server configuration from multiple sources. Config precedence (later overrides earlier): 1. Built-in DEFAULTS 2. Global config: ~/.genro-asgi/config.yaml 3. Project config: <server_dir>/config.yaml 4. Environment variables: GENRO_ASGI_* 5. Command line arguments 6. Explicit constructor parameters """ env_argv_opts = SmartOptions(_server_opts_spec, env="GENRO_ASGI", argv=argv) caller_opts = SmartOptions( dict(server_dir=server_dir, host=host, port=port, reload=reload), ignore_none=True, ) resolved_server_dir = Path(caller_opts["server_dir"] or env_argv_opts["server_dir"] or ".").resolve() # Note: sys.path modification removed - AppLoader handles isolated module loading global_config_path = Path.home() / ".genro-asgi" / "config.yaml" if global_config_path.exists(): global_config = SmartOptions(str(global_config_path)) else: global_config = SmartOptions({}) # Use --config if specified, otherwise default to config.yaml config_file = env_argv_opts["config"] or "config.yaml" project_config = SmartOptions(str(resolved_server_dir / config_file)) config = global_config + project_config server_opts = ( SmartOptions(DEFAULTS) + (global_config["server"] or SmartOptions({})) + (project_config["server"] or SmartOptions({})) + env_argv_opts + caller_opts ) server_opts["server_dir"] = resolved_server_dir config["server"] = server_opts return config @property def server(self) -> SmartOptions: """Server options (host, port, reload, server_dir).""" result: SmartOptions = self._opts["server"] return result @property def middleware(self) -> list[Any]: """Middleware configuration list.""" return self._opts["middleware"] or [] @property def plugins(self) -> SmartOptions | None: """Plugins configuration.""" result: SmartOptions | None = self._opts["plugins"] return result @property def apps(self) -> SmartOptions | None: """Apps configuration.""" result: SmartOptions | None = self._opts["apps"] return result @property def sys_apps(self) -> SmartOptions | None: """System apps configuration.""" result: SmartOptions | None = self._opts["sys_apps"] return result @property def openapi(self) -> dict[str, Any]: """OpenAPI info as plain dict.""" return self._openapi
[docs] def get_plugin_specs(self) -> dict[str, dict[str, Any]]: """Return {plugin_name: opts_dict} for all configured plugins.""" if not self.plugins: return {} result = {} for plugin_name in self.plugins.as_dict(): plugin_opts = self.plugins[plugin_name] if hasattr(plugin_opts, "as_dict"): plugin_opts = plugin_opts.as_dict() result[plugin_name] = plugin_opts or {} return result
[docs] def get_app_specs_raw(self) -> dict[str, tuple[str, str, dict[str, Any]]]: """Return {name: (module_name, class_name, kwargs)} for all configured apps. Returns raw config data without importing. Server uses AppLoader to import. """ if not self.apps: return {} result: dict[str, tuple[str, str, dict[str, Any]]] = {} for name, app_opts in self.apps.as_dict().items(): module_path, kwargs = self._parse_app_opts(name, app_opts) module_name, class_name = module_path.split(":") result[name] = (module_name, class_name, kwargs) return result
[docs] def get_sys_app_specs_raw(self) -> dict[str, tuple[str, str, dict[str, Any]]]: """Return {name: (module_name, class_name, kwargs)} for system apps. Same format as get_app_specs_raw but for sys_apps config section. """ if not self.sys_apps: return {} result: dict[str, tuple[str, str, dict[str, Any]]] = {} for name, app_opts in self.sys_apps.as_dict().items(): module_path, kwargs = self._parse_app_opts(name, app_opts) module_name, class_name = module_path.split(":") result[name] = (module_name, class_name, kwargs) return result
@property def server_dir(self) -> Path: """Return resolved server directory path.""" # server_dir is always set as Path at line 102 result: Path = self.server["server_dir"] return result def _parse_app_opts(self, name: str, app_opts: SmartOptions | dict | str | None) -> tuple[str, dict[str, Any]]: """Parse app options into module_path and kwargs. Supports convention-based loading: if module is not specified, returns empty string to signal that the loader should try the convention {name}_app:Application first. """ if app_opts is None: # Convention mode: loader will try {name}_app:Application return "", {} if isinstance(app_opts, str): return app_opts, {} if isinstance(app_opts, dict): module_path = app_opts.get("module", "") kwargs = {k: v for k, v in app_opts.items() if k != "module"} return module_path, kwargs # SmartOptions module_path = app_opts["module"] or "" kwargs = {k: v for k, v in app_opts.as_dict().items() if k != "module"} return module_path, kwargs def __getitem__(self, name: str) -> Any: """Proxy bracket access to underlying opts.""" return self._opts[name]
if __name__ == "__main__": config = ServerConfig() print(f"Server: {config.server['host']}:{config.server['port']}") print(f"Middleware: {config.middleware}") print(f"Plugins: {config.plugins}")