Address review findings on PR #147 (P2+P3)
P2-1 (containment): api/nestquest_core.py no longer mutates sys.path. Load core by file location with importlib.util.spec_from_file_location, registering it under the distinct top-level name 'nestquest_core' with submodule_search_locations pointing at the bundled core directory. No generic top-level names (db, const, schema, ...) are published that could shadow a future dependency, and the integration's HA-coupled __init__ is never executed. Use importlib.import_module for submodules; drop the redundant _BOOTSTRAPPED flag. Document that two core copies (nestquest_core.* and custom_components.nestquest.core.*) coexist in the pytest process so later tasks must not compare class/exception identity across them. P2-2 (fail fast): ApiConfig.from_env now RAISES ConfigError when NESTQUEST_DB_PATH is unset or empty — no silent fallback to a predictable world-writable temp path that loses data at reboot. Remove the module-level 'app = create_app()' (it read env at import time and would break fail-fast); document serving with 'uvicorn --factory api.app:create_app'. P3 cleanups: - /health drops db_path from the body (no filesystem path leak on an unauthenticated route) and runs a cheap SELECT 1 to reflect database reachability. - Remove dead code: resolve_db_path in api/config.py; unused LOGGER in api/app.py. - tests/test_api_health.py: add ApiConfig.from_env tests (set value, empty raises, missing raises); add startup-failure tests (unwritable dir, non-SQLite file); assert shutdown closes the connection (connected is False); make _AppRunner.__aexit__ use try/finally so a client error still shuts down the lifespan; reuse api.database._executor instead of a duplicated local copy; in the no-HA subprocess test assert core.__file__ points at the bundled directory and stop passing the unused ':memory:'. - pyproject.toml: comment that fastapi/uvicorn are dev-group only so the monorepo test venv can exercise the app; the API deployable's runtime dependencies must be declared when it is packaged (Feature 17).
This commit is contained in:
+27
-20
@@ -1,33 +1,35 @@
|
||||
"""Configuration for the NestQuest API service.
|
||||
|
||||
The API reads its single configuration knob — the SQLite database path —
|
||||
from the ``NESTQUEST_DB_PATH`` environment variable, with a documented
|
||||
default inside the OS temp directory. This mirrors the integration's
|
||||
from the ``NESTQUEST_DB_PATH`` environment variable. Unlike a dev
|
||||
convenience default, a missing or empty value is a configuration ERROR,
|
||||
not a silent fallback to a predictable world-writable temp path: the API
|
||||
owns the database (it is the writer of record per Feature 16's
|
||||
contract), and silently dropping data into ``$TMPDIR/nestquest.db``
|
||||
would lose it at reboot. ``ApiConfig.from_env`` therefore RAISES when
|
||||
the variable is unset or empty. This mirrors the integration's
|
||||
"explicit settings object" discipline (Feature 15's
|
||||
``core.settings``): the API never reads HA config; callers pass the
|
||||
resolved path into the database wiring.
|
||||
|
||||
Serving: the app is built with the factory ``api.app:create_app`` — run
|
||||
it with ``uvicorn --factory api.app:create_app`` so the factory reads
|
||||
the environment on each worker startup rather than at import time.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
#: Environment variable holding the SQLite file path. The API opens
|
||||
#: this file on startup (creating it if absent) and applies migrations.
|
||||
#: REQUIRED — unset or empty raises :class:`ConfigError` from
|
||||
#: :meth:`ApiConfig.from_env`.
|
||||
DB_PATH_ENV = "NESTQUEST_DB_PATH"
|
||||
|
||||
#: The default database location when the env var is unset. Lives in
|
||||
#: the OS temp directory so a dev run without configuration does not
|
||||
#: litter the repo, and so tests that forget to set the env var still
|
||||
#: get a hermetic-ish path. Production deployments ALWAYS set
|
||||
#: ``NESTQUEST_DB_PATH`` to a persistent volume path.
|
||||
_DEFAULT_DB_NAME = "nestquest.db"
|
||||
|
||||
|
||||
def _default_db_path() -> str:
|
||||
return str(Path(tempfile.gettempdir()) / _DEFAULT_DB_NAME)
|
||||
class ConfigError(RuntimeError):
|
||||
"""Raised when required API configuration is missing or invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -47,15 +49,20 @@ class ApiConfig:
|
||||
|
||||
``env`` defaults to ``os.environ``; tests pass an explicit dict
|
||||
so they are hermetic (no monkeypatch of the global environ).
|
||||
|
||||
Raises :class:`ConfigError` when ``NESTQUEST_DB_PATH`` is unset
|
||||
or empty/whitespace-only — there is no silent default, because a
|
||||
default in the temp directory would silently lose data at reboot.
|
||||
"""
|
||||
source = env if env is not None else os.environ
|
||||
raw = source.get(DB_PATH_ENV, "").strip()
|
||||
return cls(db_path=raw if raw else _default_db_path())
|
||||
if not raw:
|
||||
raise ConfigError(
|
||||
f"{DB_PATH_ENV} must be set to a writable SQLite file path "
|
||||
f"(it is unset or empty — the API will not fall back to a "
|
||||
f"temp directory and silently lose data)"
|
||||
)
|
||||
return cls(db_path=raw)
|
||||
|
||||
|
||||
def resolve_db_path(env: dict[str, str] | None = None) -> str:
|
||||
"""Return the resolved SQLite path (env override or documented default)."""
|
||||
return ApiConfig.from_env(env).db_path
|
||||
|
||||
|
||||
__all__ = ["ApiConfig", "DB_PATH_ENV", "resolve_db_path"]
|
||||
__all__ = ["ApiConfig", "ConfigError", "DB_PATH_ENV"]
|
||||
|
||||
Reference in New Issue
Block a user