Task_14: Pipeline -> fetch_and_store
This commit is contained in:
parent
014fbe68b4
commit
bd443fd02b
92
TASK.md
92
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?
|
||||
- Welche konkreten Vorteile hat eine schlanke `main()`-Funktion?
|
||||
- Warum bekommt `fetch_and_store()` ein fertiges `Storage`-Objekt
|
||||
übergeben — statt es selbst zu instanziieren?
|
||||
@ -10,4 +10,6 @@ active_queries:
|
||||
- bergbahn
|
||||
|
||||
storage:
|
||||
output_dir: data/results
|
||||
type: json
|
||||
params:
|
||||
output_dir: data/results
|
||||
@ -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 = []
|
||||
|
||||
@ -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}'")
|
||||
Loading…
x
Reference in New Issue
Block a user