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
|
- **ABC und `@abstractmethod`:** Eine Klasse, die von `ABC` erbt und eine
|
||||||
Verhalten der Applikation steuern (Bboxen, Timeouts, aktive Queries). In
|
`@abstractmethod` definiert, kann nicht direkt instanziiert werden —
|
||||||
Umgebungsvariablen (`.env`) gehören Secrets und Deployment-spezifische Werte
|
Python wirft einen `TypeError`. Eine Unterklasse muss *alle* abstrakten
|
||||||
(Passwörter, API-Keys, Datenbankpfade) — also alles, was nicht ins Git-Repository
|
Methoden implementieren, sonst gilt sie selbst als abstrakt.
|
||||||
soll.
|
- **Warum `store()` einen `str` zurückgibt:** Der Aufrufer bekommt einen
|
||||||
- **Ungültiger `PoiType`-Wert:** `PoiType("gondelbahn")` wirft einen `ValueError`,
|
Identifier zurück (Dateipfad, Tabellenname, URL), ohne zu wissen, welches
|
||||||
weil der Wert nicht im Enum existiert. Das ist eigentlich gut — fail fast. In
|
Backend dahintersteckt. Das ist nützlich fürs Logging und für Tests.
|
||||||
`main.py` könnte man diesen Fehler abfangen und eine sprechende Fehlermeldung
|
- **`StorageError`:** Eine eigene Exception macht den Code robuster —
|
||||||
ausgeben.
|
`main.py` muss nur `StorageError` kennen, nicht alle möglichen
|
||||||
- **`Path(__file__).parent / "config.yaml"`:** Gleiche Logik wie bei den Query-Dateien —
|
`OSError`-, `psycopg2`- oder sonstigen Backend-Fehler.
|
||||||
der Pfad wird immer relativ zur `main.py` aufgelöst, nicht zum Arbeitsverzeichnis.
|
|
||||||
|
|
||||||
|
|
||||||
## Aufgabe
|
## Aufgabe
|
||||||
|
|
||||||
Aktuell landen die gefetchten POIs nirgends — sie werden nur geloggt und dann
|
In `main.py` steht aktuell:
|
||||||
verworfen. Ziel ist es, die POIs in eine JSON-Datei zu speichern.
|
|
||||||
|
|
||||||
Wir wollen das aber so umsetzen, dass das Storage-Backend später leicht
|
```python
|
||||||
ausgetauscht werden kann (z.B. gegen eine Datenbank) — ohne `main.py` oder
|
storage = JsonStorage(output_dir=config["storage"]["output_dir"])
|
||||||
die Fetch-Logik anzufassen.
|
```
|
||||||
|
|
||||||
|
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. Lege eine neue Datei `storage.py` an.
|
1. Füge in `storage.py` eine `StorageType`-Enum hinzu:
|
||||||
2. Definiere darin eine **abstrakte Basisklasse** `Storage` (erbt von `ABC`) mit
|
|
||||||
einer abstrakten Methode:
|
|
||||||
```python
|
```python
|
||||||
@abstractmethod
|
class StorageType(StrEnum):
|
||||||
def store(self, pois: list[POI]) -> str:
|
JSON = "json"
|
||||||
...
|
POSTGRES = "postgres"
|
||||||
```
|
```
|
||||||
Die Methode soll die POIs speichern und einen Identifier zurückgeben
|
2. Schreibe eine Factory-Funktion `build_storage(cfg: dict) -> Storage`
|
||||||
(z.B. den Dateipfad oder Tabellennamen).
|
in `storage.py`, die anhand von `cfg["type"]` den richtigen Storage
|
||||||
3. Implementiere eine konkrete Klasse `JsonStorage(Storage)`:
|
instanziiert. Verwende dafür ein `match`-Statement.
|
||||||
- Nimmt einen `output_dir: str | Path` im Konstruktor entgegen.
|
3. Passe `config.yaml` an — der `storage`-Abschnitt bekommt ein `type`-Feld:
|
||||||
- `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:
|
|
||||||
```yaml
|
```yaml
|
||||||
storage:
|
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:**
|
**Fragen zum Nachdenken:**
|
||||||
- Was ist eine abstrakte Basisklasse (ABC) — und was passiert, wenn man
|
- Was ist der Vorteil eines `match`-Statements gegenüber einer
|
||||||
`Storage()` direkt instanziiert oder eine Unterklasse schreibt, die
|
`if/elif`-Kette — und ab wann lohnt sich das?
|
||||||
`store()` nicht implementiert?
|
- Was passiert, wenn jemand in `config.yaml` einen ungültigen
|
||||||
- Warum gibt `store()` einen `str` zurück (den Identifier) statt nichts (`None`)?
|
`type`-Wert einträgt (z.B. `"mongodb"`)? Wie sollte
|
||||||
- Was wäre der Nachteil, wenn `main.py` direkt `JsonStorage` instanziieren
|
`build_storage()` damit umgehen?
|
||||||
und überall verwenden würde — ohne das `Storage`-Interface?
|
- 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]
|
schweiz: [45.8, 5.9, 47.8, 10.5]
|
||||||
|
|
||||||
active_queries:
|
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 .fetcher import load_query, load_pois, OverpassApiError
|
||||||
from .models import POI, PoiType
|
from .models import POI, PoiType
|
||||||
|
from .storage import JsonStorage, StorageError
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -12,24 +13,36 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parent.parent.parent
|
||||||
|
|
||||||
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())
|
||||||
timeout = config["overpass"]["timeout"]
|
timeout = config["overpass"]["timeout"]
|
||||||
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"])
|
||||||
|
|
||||||
for poi_type in poi_types:
|
for poi_type in poi_types:
|
||||||
|
collected_pois = []
|
||||||
for name, bbox in bboxen.items():
|
for name, bbox in bboxen.items():
|
||||||
try:
|
try:
|
||||||
query = load_query(poi_type, bbox, timeout, maxsize)
|
query = load_query(poi_type, bbox, timeout, maxsize)
|
||||||
pois: list[POI] = load_pois(query=query, poi_type=poi_type)
|
pois: list[POI] = load_pois(query=query, poi_type=poi_type)
|
||||||
|
collected_pois.extend(pois)
|
||||||
except OverpassApiError as exc:
|
except OverpassApiError as exc:
|
||||||
logger.error(f"[{poi_type}] Fehler bei '{name}': {exc}")
|
logger.error(f"[{poi_type}] Fehler bei '{name}': {exc}")
|
||||||
continue
|
continue
|
||||||
logger.info(f"[{poi_type}] {name}: {len(pois)} POIs gefunden")
|
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__":
|
if __name__ == "__main__":
|
||||||
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