Task_13: Storage Factory
This commit is contained in:
parent
04e1ed8097
commit
014fbe68b4
89
TASK.md
89
TASK.md
@ -1,59 +1,66 @@
|
||||
# Task 12 — Storage-Abstraktion mit ABC
|
||||
# Task 13 — Storage-Factory und StorageType
|
||||
|
||||
## Rückblick Task 11: Externe Konfiguration
|
||||
## Rückblick Task 12: Storage-Abstraktion
|
||||
|
||||
Ihr habt alle Konfigurationswerte in eine `config.yaml` ausgelagert. Die wichtigsten Punkte:
|
||||
Ihr habt `storage.py` mit einer abstrakten Basisklasse `Storage` und einer
|
||||
konkreten `JsonStorage` eingeführt. Die wichtigsten Punkte:
|
||||
|
||||
- **Config vs. Umgebungsvariablen:** In die YAML-Datei gehören Werte, die das
|
||||
Verhalten der Applikation steuern (Bboxen, Timeouts, aktive Queries). In
|
||||
Umgebungsvariablen (`.env`) gehören Secrets und Deployment-spezifische Werte
|
||||
(Passwörter, API-Keys, Datenbankpfade) — also alles, was nicht ins Git-Repository
|
||||
soll.
|
||||
- **Ungültiger `PoiType`-Wert:** `PoiType("gondelbahn")` wirft einen `ValueError`,
|
||||
weil der Wert nicht im Enum existiert. Das ist eigentlich gut — fail fast. In
|
||||
`main.py` könnte man diesen Fehler abfangen und eine sprechende Fehlermeldung
|
||||
ausgeben.
|
||||
- **`Path(__file__).parent / "config.yaml"`:** Gleiche Logik wie bei den Query-Dateien —
|
||||
der Pfad wird immer relativ zur `main.py` aufgelöst, nicht zum Arbeitsverzeichnis.
|
||||
- **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.
|
||||
|
||||
|
||||
## Aufgabe
|
||||
|
||||
Aktuell landen die gefetchten POIs nirgends — sie werden nur geloggt und dann
|
||||
verworfen. Ziel ist es, die POIs in eine JSON-Datei zu speichern.
|
||||
In `main.py` steht aktuell:
|
||||
|
||||
Wir wollen das aber so umsetzen, dass das Storage-Backend später leicht
|
||||
ausgetauscht werden kann (z.B. gegen eine Datenbank) — ohne `main.py` oder
|
||||
die Fetch-Logik anzufassen.
|
||||
```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.
|
||||
|
||||
**Konkret:**
|
||||
|
||||
1. Lege eine neue Datei `storage.py` an.
|
||||
2. Definiere darin eine **abstrakte Basisklasse** `Storage` (erbt von `ABC`) mit
|
||||
einer abstrakten Methode:
|
||||
1. Füge in `storage.py` eine `StorageType`-Enum hinzu:
|
||||
```python
|
||||
@abstractmethod
|
||||
def store(self, pois: list[POI]) -> str:
|
||||
...
|
||||
class StorageType(StrEnum):
|
||||
JSON = "json"
|
||||
POSTGRES = "postgres"
|
||||
```
|
||||
Die Methode soll die POIs speichern und einen Identifier zurückgeben
|
||||
(z.B. den Dateipfad oder Tabellennamen).
|
||||
3. Implementiere eine konkrete Klasse `JsonStorage(Storage)`:
|
||||
- Nimmt einen `output_dir: str | Path` im Konstruktor entgegen.
|
||||
- `store()` schreibt alle POIs als JSON-Datei in dieses Verzeichnis,
|
||||
benannt nach dem `poi_type` (z.B. `bergbahn.json`).
|
||||
4. Instanziiere `JsonStorage` in `main.py` und rufe nach dem Fetchen
|
||||
`storage.store(pois)` auf.
|
||||
5. Ergänze in `config.yaml` einen `storage`-Abschnitt:
|
||||
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:
|
||||
output_dir: ./data/results
|
||||
type: json
|
||||
params:
|
||||
output_dir: ./data/results
|
||||
```
|
||||
4. Ersetze in `main.py` die direkte Instanziierung durch:
|
||||
```python
|
||||
storage = build_storage(config["storage"])
|
||||
```
|
||||
|
||||
**Fragen zum Nachdenken:**
|
||||
- Was ist eine abstrakte Basisklasse (ABC) — und was passiert, wenn man
|
||||
`Storage()` direkt instanziiert oder eine Unterklasse schreibt, die
|
||||
`store()` nicht implementiert?
|
||||
- Warum gibt `store()` einen `str` zurück (den Identifier) statt nichts (`None`)?
|
||||
- Was wäre der Nachteil, wenn `main.py` direkt `JsonStorage` instanziieren
|
||||
und überall verwenden würde — ohne das `Storage`-Interface?
|
||||
- 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?
|
||||
@ -7,4 +7,7 @@ bboxen:
|
||||
schweiz: [45.8, 5.9, 47.8, 10.5]
|
||||
|
||||
active_queries:
|
||||
- bergbahn
|
||||
- bergbahn
|
||||
|
||||
storage:
|
||||
output_dir: data/results
|
||||
@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
from .fetcher import load_query, load_pois, OverpassApiError
|
||||
from .models import POI, PoiType
|
||||
from .storage import JsonStorage, StorageError
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@ -12,24 +13,36 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).parent.parent.parent
|
||||
|
||||
def main() -> None:
|
||||
config = yaml.safe_load((Path(__file__).parent / "config.yaml").read_text())
|
||||
timeout = config["overpass"]["timeout"]
|
||||
maxsize = config["overpass"]["maxsize"]
|
||||
bboxen = config["bboxen"]
|
||||
config = yaml.safe_load((Path(__file__).parent / "config.yaml").read_text())
|
||||
timeout = config["overpass"]["timeout"]
|
||||
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"])
|
||||
|
||||
for poi_type in poi_types:
|
||||
collected_pois = []
|
||||
for name, bbox in bboxen.items():
|
||||
try:
|
||||
query = load_query(poi_type, bbox, timeout, maxsize)
|
||||
pois: list[POI] = load_pois(query=query, poi_type=poi_type)
|
||||
collected_pois.extend(pois)
|
||||
except OverpassApiError as exc:
|
||||
logger.error(f"[{poi_type}] Fehler bei '{name}': {exc}")
|
||||
continue
|
||||
logger.info(f"[{poi_type}] {name}: {len(pois)} POIs gefunden")
|
||||
for poi in pois:
|
||||
logger.debug(f" {poi.id}: ({poi.lat}, {poi.lon})")
|
||||
|
||||
if collected_pois:
|
||||
try:
|
||||
location = storage.store(collected_pois)
|
||||
logger.info(f"[{poi_type}] {len(collected_pois)} POIs gespeichert: {location}")
|
||||
except StorageError as exc:
|
||||
logger.error(f"[{poi_type}] Fehler beim Speichern: {exc}")
|
||||
else:
|
||||
logger.warning(f"[{poi_type}] Nichts zu speichern")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
src/overpass/storage.py
Normal file
36
src/overpass/storage.py
Normal file
@ -0,0 +1,36 @@
|
||||
import logging
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from .models import POI
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Storage(ABC):
|
||||
"""Abstrakte Basisklasse für POI-Storage-Backends."""
|
||||
|
||||
@abstractmethod
|
||||
def store(self, pois: list[POI]) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JsonStorage(Storage):
|
||||
def __init__(self, output_dir: str | Path):
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def store(self, pois: list[POI]) -> str:
|
||||
poi_type = pois[0].poi_type
|
||||
output_path = self.output_dir / f"{poi_type}.json"
|
||||
try:
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
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())
|
||||
Loading…
x
Reference in New Issue
Block a user