Task_14: Pipeline -> fetch_and_store

This commit is contained in:
Marco Schmid 2026-05-12 17:59:36 +02:00
parent 014fbe68b4
commit bd443fd02b
4 changed files with 68 additions and 56 deletions

92
TASK.md
View File

@ -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 Ihr habt eine Factory-Funktion `build_storage()` eingeführt. Die wichtigsten Punkte:
konkreten `JsonStorage` eingeführt. Die wichtigsten Punkte:
- **ABC und `@abstractmethod`:** Eine Klasse, die von `ABC` erbt und eine - **Factory-Pattern:** `build_storage()` zentralisiert die Entscheidung, welches
`@abstractmethod` definiert, kann nicht direkt instanziiert werden — Backend instanziiert wird. `main.py` muss weder `JsonStorage` noch
Python wirft einen `TypeError`. Eine Unterklasse muss *alle* abstrakten `PostgresStorage` kennen — es übergibt nur die Config und bekommt ein
Methoden implementieren, sonst gilt sie selbst als abstrakt. fertiges `Storage`-Objekt zurück.
- **Warum `store()` einen `str` zurückgibt:** Der Aufrufer bekommt einen - **`match`-Statement:** Klarer und erweiterbarer als eine `if/elif`-Kette,
Identifier zurück (Dateipfad, Tabellenname, URL), ohne zu wissen, welches besonders wenn neue Storage-Typen dazukommen. Der `case _`-Zweig fängt
Backend dahintersteckt. Das ist nützlich fürs Logging und für Tests. ungültige Werte ab.
- **`StorageError`:** Eine eigene Exception macht den Code robuster — - **`StorageType`-Enum:** Verhindert Magic Strings in der Factory — ein
`main.py` muss nur `StorageError` kennen, nicht alle möglichen ungültiger `type`-Wert in der Config wirft sofort einen `ValueError`.
`OSError`-, `psycopg2`- oder sonstigen Backend-Fehler. - **`**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 ## 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 Ziel ist es, die gesamte Fetch-und-Store-Logik für einen POI-Typ in eine
storage = JsonStorage(output_dir=config["storage"]["output_dir"]) eigene Funktion auszulagern.
```
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.
**Konkret:** **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 ```python
class StorageType(StrEnum): def fetch_and_store(
JSON = "json" poi_type: PoiType,
POSTGRES = "postgres" bboxen: dict,
``` timeout: int,
2. Schreibe eine Factory-Funktion `build_storage(cfg: dict) -> Storage` maxsize: int,
in `storage.py`, die anhand von `cfg["type"]` den richtigen Storage storage: Storage,
instanziiert. Verwende dafür ein `match`-Statement. ) -> None:
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"])
``` ```
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:** **Fragen zum Nachdenken:**
- Was ist der Vorteil eines `match`-Statements gegenüber einer - Welche konkreten Vorteile hat eine schlanke `main()`-Funktion?
`if/elif`-Kette — und ab wann lohnt sich das? - Warum bekommt `fetch_and_store()` ein fertiges `Storage`-Objekt
- Was passiert, wenn jemand in `config.yaml` einen ungültigen übergeben — statt es selbst zu instanziieren?
`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?

View File

@ -10,4 +10,6 @@ active_queries:
- bergbahn - bergbahn
storage: storage:
output_dir: data/results type: json
params:
output_dir: data/results

View File

@ -4,7 +4,7 @@ from pathlib import Path
from .fetcher import load_query, load_pois, OverpassApiError from .fetcher import load_query, load_pois, OverpassApiError
from .models import POI, PoiType from .models import POI, PoiType
from .storage import JsonStorage, StorageError from .storage import build_storage, StorageError
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -13,7 +13,7 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ROOT = Path(__file__).parent.parent.parent ROOT = Path(__file__).parent.parent.parent # → project/
def main() -> None: def main() -> None:
config = yaml.safe_load((Path(__file__).parent / "config.yaml").read_text()) config = yaml.safe_load((Path(__file__).parent / "config.yaml").read_text())
@ -21,7 +21,7 @@ def main() -> None:
maxsize = config["overpass"]["maxsize"] maxsize = config["overpass"]["maxsize"]
bboxen = config["bboxen"] bboxen = config["bboxen"]
poi_types = [PoiType(pt) for pt in config["active_queries"]] 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: for poi_type in poi_types:
collected_pois = [] collected_pois = []

View File

@ -2,6 +2,7 @@ import logging
import json import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import asdict from dataclasses import asdict
from enum import StrEnum
from pathlib import Path from pathlib import Path
from .models import POI from .models import POI
@ -12,6 +13,11 @@ class StorageError(Exception):
pass pass
class StorageType(StrEnum):
JSON = "json"
POSTGRES = "postgres"
class Storage(ABC): class Storage(ABC):
"""Abstrakte Basisklasse für POI-Storage-Backends.""" """Abstrakte Basisklasse für POI-Storage-Backends."""
@ -34,3 +40,17 @@ class JsonStorage(Storage):
except OSError as exc: except OSError as exc:
raise StorageError("Fehler beim Speichern der POIs") from exc raise StorageError("Fehler beim Speichern der POIs") from exc
return str(output_path.resolve()) 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}'")