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
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?
- Welche konkreten Vorteile hat eine schlanke `main()`-Funktion?
- Warum bekommt `fetch_and_store()` ein fertiges `Storage`-Objekt
übergeben — statt es selbst zu instanziieren?

View File

@ -10,4 +10,6 @@ active_queries:
- bergbahn
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 .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 = []

View File

@ -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())
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}'")