diff --git a/TASK.md b/TASK.md index 2d4967c..d11af9c 100644 --- a/TASK.md +++ b/TASK.md @@ -1,66 +1,56 @@ -# Task 13 — Storage-Factory und StorageType +# Task 14 — pipeline.py: fetch_and_store() -## Rückblick Task 12: Storage-Abstraktion +## Rückblick Task 13: Storage-Factory -Ihr habt `storage.py` mit einer abstrakten Basisklasse `Storage` und einer -konkreten `JsonStorage` eingeführt. Die wichtigsten Punkte: +Ihr habt eine Factory-Funktion `build_storage()` eingeführt. Die wichtigsten Punkte: -- **ABC und `@abstractmethod`:** Eine Klasse, die von `ABC` erbt und eine - `@abstractmethod` definiert, kann nicht direkt instanziiert werden — - Python wirft einen `TypeError`. Eine Unterklasse muss *alle* abstrakten - Methoden implementieren, sonst gilt sie selbst als abstrakt. -- **Warum `store()` einen `str` zurückgibt:** Der Aufrufer bekommt einen - Identifier zurück (Dateipfad, Tabellenname, URL), ohne zu wissen, welches - Backend dahintersteckt. Das ist nützlich fürs Logging und für Tests. -- **`StorageError`:** Eine eigene Exception macht den Code robuster — - `main.py` muss nur `StorageError` kennen, nicht alle möglichen - `OSError`-, `psycopg2`- oder sonstigen Backend-Fehler. +- **Factory-Pattern:** `build_storage()` zentralisiert die Entscheidung, welches + Backend instanziiert wird. `main.py` muss weder `JsonStorage` noch + `PostgresStorage` kennen — es übergibt nur die Config und bekommt ein + fertiges `Storage`-Objekt zurück. +- **`match`-Statement:** Klarer und erweiterbarer als eine `if/elif`-Kette, + besonders wenn neue Storage-Typen dazukommen. Der `case _`-Zweig fängt + ungültige Werte ab. +- **`StorageType`-Enum:** Verhindert Magic Strings in der Factory — ein + ungültiger `type`-Wert in der Config wirft sofort einen `ValueError`. +- **`**params`:** Die Parameter aus der Config werden direkt als Keyword-Argumente + an den Konstruktor übergeben. Das macht `build_storage()` generisch — + sie muss nicht wissen, welche Parameter `JsonStorage` oder `PostgresStorage` + konkret erwarten. +- **`root`-Parameter:** Die Pfadauflösung bleibt in `main.py` verankert — + `storage.py` weiss nichts von der Projektstruktur, was die Wiederverwendbarkeit + erhöht. +--- ## Aufgabe -In `main.py` steht aktuell: +In `main.py` ist die Fetch-Schleife über Bboxen direkt in `main()` eingebettet. +Das hat einen Nachteil: Die Logik ist schwer isoliert testbar, und `main()` +wird mit jedem Feature länger und unübersichtlicher. -```python -storage = JsonStorage(output_dir=config["storage"]["output_dir"]) -``` - -Das bedeutet: Will man später ein anderes Backend verwenden, muss `main.py` -angefasst werden. Ausserdem muss `main.py` wissen, welche Klasse (`JsonStorage`, -`PostgresStorage`, ...) zu welchem Config-Wert gehört. - -Ziel ist eine **Factory-Funktion** `build_storage(cfg)`, die diese Entscheidung -übernimmt — `main.py` übergibt nur die Config und bekommt ein fertiges -`Storage`-Objekt zurück. +Ziel ist es, die gesamte Fetch-und-Store-Logik für einen POI-Typ in eine +eigene Funktion auszulagern. **Konkret:** -1. Füge in `storage.py` eine `StorageType`-Enum hinzu: +1. Lege eine neue Datei `pipeline.py` an. +2. Schreibe darin eine Funktion `fetch_and_store()` mit folgender Signatur: ```python - class StorageType(StrEnum): - JSON = "json" - POSTGRES = "postgres" -``` -2. Schreibe eine Factory-Funktion `build_storage(cfg: dict) -> Storage` - in `storage.py`, die anhand von `cfg["type"]` den richtigen Storage - instanziiert. Verwende dafür ein `match`-Statement. -3. Passe `config.yaml` an — der `storage`-Abschnitt bekommt ein `type`-Feld: -```yaml - storage: - type: json - params: - output_dir: ./data/results -``` -4. Ersetze in `main.py` die direkte Instanziierung durch: -```python - storage = build_storage(config["storage"]) + def fetch_and_store( + poi_type: PoiType, + bboxen: dict, + timeout: int, + maxsize: int, + storage: Storage, + ) -> None: ``` + Sie soll die bisherige Schleife aus `main.py` übernehmen: + über alle Bboxen iterieren, POIs fetchen, sammeln und am Ende speichern. +3. Vereinfache `main()` so, dass sie nur noch Config liest, Storage baut + und `fetch_and_store()` pro POI-Typ aufruft. **Fragen zum Nachdenken:** -- Was ist der Vorteil eines `match`-Statements gegenüber einer - `if/elif`-Kette — und ab wann lohnt sich das? -- Was passiert, wenn jemand in `config.yaml` einen ungültigen - `type`-Wert einträgt (z.B. `"mongodb"`)? Wie sollte - `build_storage()` damit umgehen? -- Warum übergeben wir `params` als `**params` an den Konstruktor — - und was ist der Vorteil gegenüber einzelnen Parametern? \ No newline at end of file +- Welche konkreten Vorteile hat eine schlanke `main()`-Funktion? +- Warum bekommt `fetch_and_store()` ein fertiges `Storage`-Objekt + übergeben — statt es selbst zu instanziieren? \ No newline at end of file diff --git a/src/overpass/config.yaml b/src/overpass/config.yaml index c7b62f8..931eb17 100644 --- a/src/overpass/config.yaml +++ b/src/overpass/config.yaml @@ -10,4 +10,6 @@ active_queries: - bergbahn storage: - output_dir: data/results \ No newline at end of file + type: json + params: + output_dir: data/results \ No newline at end of file diff --git a/src/overpass/main.py b/src/overpass/main.py index 6e02bd9..b400300 100644 --- a/src/overpass/main.py +++ b/src/overpass/main.py @@ -4,7 +4,7 @@ from pathlib import Path from .fetcher import load_query, load_pois, OverpassApiError from .models import POI, PoiType -from .storage import JsonStorage, StorageError +from .storage import build_storage, StorageError logging.basicConfig( level=logging.INFO, @@ -13,7 +13,7 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -ROOT = Path(__file__).parent.parent.parent +ROOT = Path(__file__).parent.parent.parent # → project/ def main() -> None: config = yaml.safe_load((Path(__file__).parent / "config.yaml").read_text()) @@ -21,7 +21,7 @@ def main() -> None: maxsize = config["overpass"]["maxsize"] bboxen = config["bboxen"] poi_types = [PoiType(pt) for pt in config["active_queries"]] - storage = JsonStorage(output_dir=ROOT / config["storage"]["output_dir"]) + storage = build_storage(config["storage"], root=ROOT) for poi_type in poi_types: collected_pois = [] diff --git a/src/overpass/storage.py b/src/overpass/storage.py index 2b10a34..5d8f0d8 100644 --- a/src/overpass/storage.py +++ b/src/overpass/storage.py @@ -2,6 +2,7 @@ import logging import json from abc import ABC, abstractmethod from dataclasses import asdict +from enum import StrEnum from pathlib import Path from .models import POI @@ -12,6 +13,11 @@ class StorageError(Exception): pass +class StorageType(StrEnum): + JSON = "json" + POSTGRES = "postgres" + + class Storage(ABC): """Abstrakte Basisklasse für POI-Storage-Backends.""" @@ -33,4 +39,18 @@ class JsonStorage(Storage): json.dump([asdict(poi) for poi in pois], f, indent=2, ensure_ascii=False) except OSError as exc: raise StorageError("Fehler beim Speichern der POIs") from exc - return str(output_path.resolve()) \ No newline at end of file + return str(output_path.resolve()) + + +def build_storage(cfg: dict, root: Path) -> Storage: + """Factory: liest config-Dict und gibt fertigen Storage zurück.""" + storage_type = StorageType(cfg["type"]) + params = cfg.get("params", {}) + + match storage_type: + case StorageType.JSON: + return JsonStorage(output_dir=root / params["output_dir"]) + case StorageType.POSTGRES: + raise NotImplementedError("PostgresStorage folgt später") + case _: + raise ValueError(f"Unbekannter Storage-Typ: '{storage_type}'") \ No newline at end of file