Task_12: Speichern der Daten (Storage Abstraktion)
This commit is contained in:
parent
edf7893b27
commit
04e1ed8097
95
TASK.md
95
TASK.md
@ -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
10
src/overpass/config.yaml
Normal 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
|
||||
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user