harden delivery pipeline and production runtime
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-11 21:14:07 +02:00
parent 471146761e
commit aaf319ff14
26 changed files with 202 additions and 73 deletions

View File

@@ -31,10 +31,13 @@ class HaClient:
"Content-Type": "application/json",
})
def close(self) -> None:
self._session.close()
def list_entities(self) -> list[dict[str, object]]:
try:
response = self._session.get(
f"{self._settings.url}/api/states",
f"{self._settings.url.rstrip('/')}/api/states",
timeout=self._settings.timeout_seconds,
)
except requests.Timeout as exc:
@@ -71,4 +74,4 @@ class HaClient:
"Antwort von Home Assistant hat unerwartetes Format."
)
return payload
return payload

View File

@@ -15,9 +15,10 @@ class HaReader:
entities = self._client.list_entities()
summaries: list[HaEntitySummary] = []
for item in entities:
entity_id = item.get("entity_id", "")
if "." not in entity_id:
raw_entity_id = item.get("entity_id")
if not isinstance(raw_entity_id, str) or "." not in raw_entity_id:
continue
entity_id = raw_entity_id
domain = entity_id.split(".", 1)[0]
raw_attributes = item.get("attributes") or {}
attributes: dict[str, Any] = raw_attributes if isinstance(raw_attributes, dict) else {}

View File

@@ -1,4 +1,6 @@
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from typing import cast
from fastapi import FastAPI
@@ -7,23 +9,30 @@ from app.config import load_settings
from app.core.exception_handlers import register_exception_handlers
from app.ha.client import HaClient, HaClientSettings
from app.ha.reader import HaReader
from app.ml.registry.model_registry import ModelRegistry
from backend.routes.ml import init_ml_routes
@asynccontextmanager
async def lifespan(app: FastAPI): # type: ignore[no-untyped-def]
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
settings = app.state.settings
client: HaClient | None = None
app.state.registry = ModelRegistry(settings.model_store)
if hasattr(app.state, "ha_reader"):
del app.state.ha_reader
if settings.ha_configured:
client = HaClient(
settings=HaClientSettings(
url=settings.ha_url,
token=settings.ha_token,
url=cast(str, settings.ha_url),
token=cast(str, settings.ha_token),
)
)
app.state.ha_reader = HaReader(client=client)
yield
try:
yield
finally:
if client is not None:
client.close()
app = FastAPI(

View File

@@ -1,5 +1,5 @@
"""Machine-Learning-Grundbausteine für SillyHome Next."""
__all__ = ["FeatureStore", "FeatureVector"]
__all__ = ["FeatureStore", "FeatureVector", "TrainedArtifact", "TrainingPipeline"]
from app.ml.feature_store import FeatureStore, FeatureVector
from app.ml.training import TrainedArtifact, TrainingPipeline

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Iterable
from collections.abc import Iterable
from dataclasses import dataclass
@dataclass(frozen=True)
@@ -28,4 +28,4 @@ class FeatureStore:
return series[-1] if series else None
def all(self) -> list[FeatureVector]:
return [vector for vectors in self._vectors.values() for vector in vectors]
return [vector for vectors in self._vectors.values() for vector in vectors]

View File

@@ -23,8 +23,8 @@ class ModelRegistry:
def register(self, artifact: TrainedArtifact) -> TrainedArtifact:
self._validate_artifact_id(artifact.artifact_id)
self._artifacts[artifact.artifact_id] = artifact
self._persist(artifact)
self._artifacts[artifact.artifact_id] = artifact
return artifact
def load_artifact(self, artifact_id: str) -> TrainedArtifact:

View File

@@ -2,9 +2,8 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Sequence
from app.ml.feature_store import FeatureVector, FeatureStore
from app.ml.feature_store import FeatureStore
logger = logging.getLogger(__name__)
@@ -34,4 +33,4 @@ class TrainingPipeline:
def export(self, artifact_id: str) -> TrainedArtifact:
if artifact_id not in self._artifacts:
raise KeyError(f"Artifact '{artifact_id}' nicht gefunden.")
return self._artifacts[artifact_id]
return self._artifacts[artifact_id]