Ctrl K
ComunidadGuías…

core/config_preflight.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Ali Sadeghi Aghili
"""Refuse to start on an unreadable ``project_config/`` with one clear line.

``security.sql_guard`` (and through it everything the API imports) binds
``schema_data.columns.TABLE_COLUMNS`` at import time, so a missing or broken
``schema.yaml`` -- or any other file the import chain reads -- used to end
``uvicorn api.server:app`` and ``scripts/prompt_budget.py`` in a forty-line
traceback whose last line is the only useful one. This module runs the real
loaders for those files *before* the imports that read them and turns a
failure into a single line naming the file and the error.

It reads the files exactly as the application does (the same loaders, the
same strict YAML parser), so it can never accept a file the application
would refuse, nor refuse one it would accept. It imports only what the
caller is about to import anyway -- ``knowledge.config_loader`` and
``schema_data.registry`` -- and nothing heavy (no web framework, no
database driver).

Public API
----------
SERVER_IMPORT_FILES
    The files ``import api.server`` reads.
PROMPT_BUDGET_IMPORT_FILES
    The files ``scripts/prompt_budget.py`` reads while importing.
project_config_problem(filenames) -> str | None
    The first problem found, as one line, or ``None``.
exit_if_project_config_unusable(filenames, *, hints=(), status=2) -> None
    Print the problem to stderr and raise ``SystemExit``; do nothing when
    every file loads.
"""

from __future__ import annotations

import sys
from collections.abc import Callable, Iterable

__all__ = [
    "PROMPT_BUDGET_IMPORT_FILES",
    "SERVER_IMPORT_FILES",
    "exit_if_project_config_unusable",
    "project_config_problem",
]

#: What ``import api.server`` reads: ``retrieval/*``, ``session/*``,
#: ``prompt_engine/static_prefix.py`` and ``security/sql_guard.py`` bind
#: these at import time. (Files read later -- the system prompt, the memory
#: policy -- fail at start-up with their own message.) Pinned to a real
#: import by ``tests/test_config_preflight.py``.
SERVER_IMPORT_FILES: tuple[str, ...] = (
    "entities.yaml",
    "schema.yaml",
    "retrieval_hints.yaml",
    "aliases.yaml",
    "business_rules.yaml",
    "examples.yaml",
    "metrics.yaml",
    "session_policy.yaml",
)

#: What ``scripts/prompt_budget.py`` reads while importing.
PROMPT_BUDGET_IMPORT_FILES: tuple[str, ...] = (
    "business_rules.yaml",
    "examples.yaml",
    "metrics.yaml",
    "schema.yaml",
)


def _loaders() -> dict[str, Callable[[], object]]:
    """File name -> the function the application reads that file with."""
    from knowledge.config_loader import (
        load_aliases,
        load_business_rules,
        load_entities,
        load_examples,
        load_metrics,
        load_retrieval_hints,
        load_session_policy,
    )
    from schema_data.registry import load_schema

    return {
        "aliases.yaml": load_aliases,
        "entities.yaml": load_entities,
        "business_rules.yaml": load_business_rules,
        "examples.yaml": load_examples,
        "metrics.yaml": load_metrics,
        "schema.yaml": load_schema,
        "retrieval_hints.yaml": load_retrieval_hints,
        "session_policy.yaml": load_session_policy,
    }


def project_config_problem(filenames: Iterable[str]) -> str | None:
    """Return the first problem with *filenames* under ``project_config/``.

    Args:
        filenames: Names from :func:`_loaders`, checked in the order given.

    Returns:
        ``None`` when every file loads. Otherwise the loader's own message,
        which names the file (``[schema.yaml] duplicate key ...`` or
        ``<path> not found. ...``); a message that does not mention the
        file is prefixed with ``[<name>]``.

    Raises:
        ImportError: If the loaders themselves cannot be imported (a broken
            install, not a configuration problem).

    Examples:
        >>> import tempfile
        >>> from config import override_settings
        >>> with tempfile.TemporaryDirectory() as empty:
        ...     with override_settings(project_config_dir=empty):
        ...         message = project_config_problem(["schema.yaml"])
        >>> "schema.yaml" in message and "not found" in message
        True
    """
    from knowledge.config_loader import ConfigNotFoundError

    loaders = _loaders()
    for name in filenames:
        try:
            loaders[name]()
        except (ConfigNotFoundError, ValueError, OSError) as exc:
            message = " ".join(str(exc).split())
            return message if name in message else f"[{name}] {message}"
    return None


def exit_if_project_config_unusable(
    filenames: Iterable[str],
    *,
    hints: Iterable[str] = (),
    status: int = 2,
) -> None:
    """Print ``cannot load project_config: <problem>`` and exit, or return.

    Nothing happens when every file in *filenames* loads, so a valid
    deployment starts exactly as before.

    Args:
        filenames: Files to check, in order; see
            :func:`project_config_problem`.
        hints: Extra lines printed after the first one.
        status: The ``SystemExit`` status; ``2`` like ``scripts/sync_schema.py``.

    Raises:
        SystemExit: With *status*, after writing to stderr, when a file
            cannot be loaded.
    """
    problem = project_config_problem(filenames)
    if problem is None:
        return
    print(f"cannot load project_config: {problem}", file=sys.stderr)
    for line in hints:
        print(line, file=sys.stderr)
    raise SystemExit(status)