Task_12: Speichern der Daten (Storage Abstraktion)

This commit is contained in:
Marco Schmid 2026-05-12 17:23:05 +02:00
parent edf7893b27
commit 04e1ed8097
3 changed files with 74 additions and 69 deletions

95
TASK.md
View File

@ -1,66 +1,59 @@
# Task 11 — Externe Konfiguration mit config.yaml
# Task 12 — Storage-Abstraktion mit ABC
## Rückblick Task 10: Queries auslagern
## Rückblick Task 11: Externe Konfiguration
Ihr habt die Overpass-Queries in eigene `.overpassql`-Dateien verschoben und
`load_query()` in `fetcher.py` eingeführt. Die wichtigsten Punkte:
Ihr habt alle Konfigurationswerte in eine `config.yaml` ausgelagert. Die wichtigsten Punkte:
- **Separation of Concerns:** Die Query ist jetzt klar vom Python-Code getrennt.
Jemand kann eine neue Query schreiben, ohne `fetcher.py` anzufassen — und umgekehrt
kann `fetcher.py` verbessert werden, ohne die Queries zu kennen.
- **`Path(__file__).parent / "queries"`** ist robuster als `"queries/"`, weil er
immer relativ zur Datei selbst aufgelöst wird — unabhängig davon, aus welchem
Verzeichnis das Skript gestartet wird.
- **Fehlender Query-File:** `load_query()` prüft explizit, ob die Datei existiert,
und wirft eine sprechende `OverpassApiError`. Ohne diese Prüfung käme ein
generischer `FileNotFoundError` — schwerer zu debuggen.
- **`{timeout}` und `{maxsize}` im Template:** Statt Werte im Query-String
hardzucoden, werden sie beim Laden eingefüllt. Das macht die Query flexibel
und die Werte zentral steuerbar.
- **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.
## Aufgabe
In `main.py` stehen noch immer Konfigurationswerte direkt im Code:
Aktuell landen die gefetchten POIs nirgends — sie werden nur geloggt und dann
verworfen. Ziel ist es, die POIs in eine JSON-Datei zu speichern.
```python
BBOXEN = {
"davos": (46.72, 9.70, 46.92, 10.00),
"schweiz": (45.8, 5.9, 47.8, 10.5),
}
TIMEOUT = 25
MAXSIZE = 5000000
poi_type = PoiType.BERGBAHN
```
Wer eine neue Bbox hinzufügen oder einen anderen POI-Typ abfragen will,
muss Python-Code editieren. Das ist unpraktisch — und fehleranfällig.
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.
**Konkret:**
1. Lege eine `config.yaml` im Package-Ordner an mit folgender Struktur:
1. Lege eine neue Datei `storage.py` an.
2. Definiere darin eine **abstrakte Basisklasse** `Storage` (erbt von `ABC`) mit
einer abstrakten Methode:
```python
@abstractmethod
def store(self, pois: list[POI]) -> str:
...
```
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:
```yaml
overpass:
timeout: 25
maxsize: 5000000
bboxen:
davos: [46.72, 9.70, 46.92, 10.00]
schweiz: [45.8, 5.9, 47.8, 10.5]
active_queries:
- bergbahn
storage:
output_dir: ./data/results
```
2. Installiere `PyYAML` falls noch nicht vorhanden (`pip install pyyaml`).
3. Lese die Config in `main.py` mit `yaml.safe_load()` ein.
4. Ersetze alle hardcodierten Konstanten durch die Werte aus der Config.
5. Erzeuge `poi_types` als Liste von `PoiType`-Objekten aus `active_queries`
und iteriere in `main()` darüber. Damit können wir nachher nicht nur z.B. Restaurants fetchen, sondern zusätzlich
auch andere POI-Typen.
**Fragen zum Nachdenken:**
- Welche Arten von Konfiguration gehören in eine YAML-Datei, welche eher
in Umgebungsvariablen (`.env`)?
- Was passiert, wenn jemand in `active_queries` einen ungültigen Wert einträgt
(z.B. `"gondelbahn"`), der nicht im `PoiType`-Enum existiert?
- 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?

10
src/overpass/config.yaml Normal file
View File

@ -0,0 +1,10 @@
overpass:
timeout: 25
maxsize: 5000000
bboxen:
davos: [46.72, 9.70, 46.92, 10.00]
schweiz: [45.8, 5.9, 47.8, 10.5]
active_queries:
- bergbahn

View File

@ -1,4 +1,7 @@
import yaml
import logging
from pathlib import Path
from .fetcher import load_query, load_pois, OverpassApiError
from .models import POI, PoiType
@ -9,25 +12,24 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
BBOXEN = {
"davos": (46.72, 9.70, 46.92, 10.00),
"schweiz": (45.8, 5.9, 47.8, 10.5),
}
TIMEOUT = 25
MAXSIZE = 5000000
poi_type = PoiType.BERGBAHN
def main() -> None:
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)
except OverpassApiError as exc:
logger.error(f"Fehler bei '{name}': {exc}")
continue
logger.info(f"{name}: {len(pois)} POIs gefunden")
for poi in pois:
logger.debug(f" {poi.id}: ({poi.lat}, {poi.lon})")
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"]]
for poi_type in poi_types:
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)
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 __name__ == "__main__":
main()