From 24be7a4f11fa05faf07a720c7bcb8e23c617b893 Mon Sep 17 00:00:00 2001 From: Pino Date: Thu, 11 Jun 2026 00:21:21 +0200 Subject: [PATCH] ML-002: Trainingspipeline mit Tainted-Data-Check --- app/ml/__init__.py | 6 ++--- app/ml/training.py | 37 ++++++++++++++++++++++++++++++ tests/ml/test_training.py | 48 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 app/ml/training.py create mode 100644 tests/ml/test_training.py diff --git a/app/ml/__init__.py b/app/ml/__init__.py index 1a48763..c9e9f5e 100644 --- a/app/ml/__init__.py +++ b/app/ml/__init__.py @@ -1,5 +1,5 @@ + """Machine-Learning-Grundbausteine für SillyHome Next.""" - +__all__ = ["FeatureStore", "FeatureVector"] from app.ml.feature_store import FeatureStore, FeatureVector - -__all__ = ["FeatureStore", "FeatureVector"] \ No newline at end of file +from app.ml.training import TrainedArtifact, TrainingPipeline diff --git a/app/ml/training.py b/app/ml/training.py new file mode 100644 index 0000000..33eb84a --- /dev/null +++ b/app/ml/training.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Sequence + +from app.ml.feature_store import FeatureVector, FeatureStore + +logger = logging.getLogger(__name__) + + +@dataclass +class TrainedArtifact: + artifact_id: str + supported_sensors: tuple[str, ...] + + +class TrainingPipeline: + def __init__(self, store: FeatureStore) -> None: + self._store = store + self._artifacts: dict[str, TrainedArtifact] = {} + + def run(self, artifact_id: str) -> TrainedArtifact: + vectors = self._store.all() + if not vectors: + raise ValueError("FeatureStore enthält keine Trainingsdaten.") + + sensors = tuple({vector.sensor_id for vector in vectors}) + artifact = TrainedArtifact(artifact_id=artifact_id, supported_sensors=sensors) + self._artifacts[artifact_id] = artifact + logger.info("Training abgeschlossen für %s mit %d Sensoren", artifact_id, len(sensors)) + return artifact + + 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] \ No newline at end of file diff --git a/tests/ml/test_training.py b/tests/ml/test_training.py new file mode 100644 index 0000000..7a9256c --- /dev/null +++ b/tests/ml/test_training.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import pytest + +from app.ml.feature_store import FeatureStore, FeatureVector +from app.ml.training import TrainingPipeline, TrainedArtifact + + +def _vector(sensor_id: str, temperature: float, label: str | None = None) -> FeatureVector: + return FeatureVector(sensor_id=sensor_id, values={"temperature": temperature}, label=label) + + +def store_with_data() -> TrainingPipeline: + store = FeatureStore() + store.add_batch( + [ + _vector("sensor.kitchen", 19.0), + _vector("sensor.kitchen", 20.0), + _vector("sensor.bedroom", 18.5), + ] + ) + return TrainingPipeline(store) + + +def test_run_returns_trained_artifact() -> None: + pipeline = store_with_data() + artifact = pipeline.run("artifact_v1") + assert artifact.artifact_id == "artifact_v1" + assert artifact.supported_sensors == ("sensor.bedroom", "sensor.kitchen") + + +def test_run_without_data_raises_value_error() -> None: + pipeline = TrainingPipeline(FeatureStore()) + with pytest.raises(ValueError): + pipeline.run("artifact_v1") + + +def test_export_returns_registered_artifact() -> None: + pipeline = store_with_data() + pipeline.run("artifact_v1") + exported = pipeline.export("artifact_v1") + assert exported == pipeline.export("artifact_v1") + + +def test_export_missing_artifact_raises_key_error() -> None: + pipeline = store_with_data() + with pytest.raises(KeyError): + pipeline.export("artifact_v1") \ No newline at end of file