Merge pull request 'process orders with error handling' (#7) from process_orders_error_handling into master
Reviewed-on: #7
This commit is contained in:
commit
5d0aa9c876
0
src/order5_initial/__init__.py
Normal file
0
src/order5_initial/__init__.py
Normal file
4253
src/order5_initial/data/orders_1.json
Normal file
4253
src/order5_initial/data/orders_1.json
Normal file
File diff suppressed because it is too large
Load Diff
4254
src/order5_initial/data/orders_2.json
Normal file
4254
src/order5_initial/data/orders_2.json
Normal file
File diff suppressed because it is too large
Load Diff
4163
src/order5_initial/data/orders_3.json
Normal file
4163
src/order5_initial/data/orders_3.json
Normal file
File diff suppressed because it is too large
Load Diff
4253
src/order5_initial/data/orders_4.json
Normal file
4253
src/order5_initial/data/orders_4.json
Normal file
File diff suppressed because it is too large
Load Diff
228
src/order5_initial/main.py
Normal file
228
src/order5_initial/main.py
Normal file
@ -0,0 +1,228 @@
|
||||
"""
|
||||
main.py — Bestellungen einlesen und validieren
|
||||
Demonstriert:
|
||||
- Standard-Logging (logging-Modul) mit FileHandler + StreamHandler
|
||||
- Sauberes Exception-Handling für UnicodeDecodeError & json.JSONDecodeError
|
||||
- Eigene Exception-Klasse InvalidOrderError (erbt von ValueError)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Logging-Konfiguration
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def setup_logger(name: str = None, log_file: str = "orders.log") -> logging.Logger:
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(levelname)s %(name)s %(message)s",
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
return logger
|
||||
|
||||
|
||||
LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s"
|
||||
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
def setup_logger_extended(name: str, log_file: str = "orders.log") -> logging.Logger:
|
||||
"""
|
||||
Erstellt und konfiguriert einen Logger mit zwei Handlern:
|
||||
- StreamHandler → Ausgabe auf die Konsole (ab INFO)
|
||||
- FileHandler → Ausgabe in eine Log-Datei (ab DEBUG)
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.DEBUG) # Root-Level: alles durchlassen
|
||||
|
||||
# Konsole: INFO und höher
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
stream_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
|
||||
|
||||
# Log-File: DEBUG und höher (detaillierter)
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
|
||||
|
||||
logger.addHandler(stream_handler)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# JSON-File einlesen
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def load_orders(path: str | Path, logger: logging.Logger) -> list[dict] | None:
|
||||
"""
|
||||
Liest eine JSON-Datei mit Bestellungen ein.
|
||||
|
||||
Behandelte Fehler
|
||||
-----------------
|
||||
UnicodeDecodeError → Falsche Kodierung (z. B. Latin-1 statt UTF-8)
|
||||
json.JSONDecodeError → Ungültiges JSON (Syntaxfehler)
|
||||
|
||||
Rückgabe
|
||||
--------
|
||||
Liste der Bestellungen bei Erfolg, None bei Fehler.
|
||||
"""
|
||||
path = Path(path)
|
||||
logger.info("Lese Datei: %s", path)
|
||||
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
loaded_file = json.load(f)
|
||||
logger.info(f"{path} erfolgreich eingelesen")
|
||||
return loaded_file
|
||||
except (
|
||||
FileNotFoundError,
|
||||
PermissionError,
|
||||
json.JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
) as ex:
|
||||
# logger.error(f"Fehler beim Lesen von File: %s", path)
|
||||
logger.error(f"{ex.__class__.__name__}: {str(ex)}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Eigene Exception-Klasse
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class InvalidOrderError(ValueError):
|
||||
"""
|
||||
Wird ausgelöst, wenn eine Bestellung ungültige Geschäftsdaten enthält.
|
||||
Erbt von ValueError, weil es sich um einen inhaltlichen Wertfehler handelt
|
||||
(kein technisches I/O-Problem).
|
||||
|
||||
Attribute
|
||||
---------
|
||||
order_id : str
|
||||
Die ID der fehlerhaften Bestellung.
|
||||
field : str
|
||||
Der Name des ungültigen Feldes (z. B. "qty").
|
||||
value : object
|
||||
Der tatsächliche (ungültige) Wert.
|
||||
message : str
|
||||
Lesbare Fehlerbeschreibung (auch als str(e) verfügbar).
|
||||
"""
|
||||
|
||||
def __init__(self, order_id: str, field: str, value: object, message: str):
|
||||
self.order_id = order_id
|
||||
self.field = field
|
||||
self.value = value
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"InvalidOrderError | order_id={self.order_id!r} "
|
||||
f"| field={self.field!r} | value={self.value!r} "
|
||||
f"| {self.message}"
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Geschäftslogik: Validierung einer einzelnen Bestellung
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def validate_order(order: dict) -> None:
|
||||
"""
|
||||
Prüft eine einzelne Bestellung auf Plausibilität.
|
||||
Wirft InvalidOrderError, sobald ein Regelverstoß entdeckt wird.
|
||||
|
||||
Regeln (erweiterbar):
|
||||
- qty darf nicht negativ sein
|
||||
- total_chf darf nicht negativ sein
|
||||
- order_id und status müssen vorhanden sein
|
||||
"""
|
||||
order_id = order.get("order_id", "<unbekannt>")
|
||||
if order_id == "<unbekannt>":
|
||||
raise InvalidOrderError(
|
||||
order_id, "order_id", order_id, "order_id muss vorhanden sein"
|
||||
)
|
||||
|
||||
order_state = order.get("status", "<unbekannt>")
|
||||
if not order_state:
|
||||
raise InvalidOrderError(
|
||||
order_state, "order_state", order_state, "order_state muss vorhanden sein"
|
||||
)
|
||||
|
||||
# loop über alle items in einem order -> falls Kontrolle (Regeln!) nicht eingehalten werden -> raise InvalidOrderError()
|
||||
for item in order.get("items", []):
|
||||
qty = item.get("qty")
|
||||
if not qty or qty < 1:
|
||||
raise InvalidOrderError(order_id, "qty", qty, "qty darf nicht negativ sein")
|
||||
|
||||
total_chf = order.get("total_chf")
|
||||
if not total_chf or total_chf < 0:
|
||||
raise InvalidOrderError(
|
||||
order_id, "total_chf", total_chf, "total_chf darf nicht negativ sein"
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Validierungs-Durchlauf über alle Bestellungen
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def process_orders(orders: list[dict], logger: logging.Logger) -> None:
|
||||
"""
|
||||
Iteriert über alle Bestellungen und validiert jede einzelne.
|
||||
Ungültige Bestellungen werden geloggt und übersprungen (kein Abbruch).
|
||||
"""
|
||||
valid_count = 0
|
||||
invalid_count = 0
|
||||
|
||||
for order in orders.get("orders", []):
|
||||
try:
|
||||
validate_order(order)
|
||||
valid_count += 1
|
||||
logger.info("OK: %s", order.get("order_id"))
|
||||
except InvalidOrderError as ex:
|
||||
invalid_count += 1
|
||||
logger.error("NOK: %s", str(ex))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger = setup_logger_extended(name="order")
|
||||
# logger = setup_logger()
|
||||
|
||||
files = [
|
||||
"orders_1.json",
|
||||
"orders_5.json",
|
||||
"orders_2.json",
|
||||
"orders_3.json",
|
||||
"orders_4.json",
|
||||
]
|
||||
|
||||
for filename in files:
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
file_path = BASE_DIR / "data" / filename
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("Verarbeite: %s", filename)
|
||||
|
||||
orders = load_orders(file_path, logger)
|
||||
|
||||
if orders is not None:
|
||||
process_orders(orders, logger)
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"Alle {len(files)} Dateien verarbeitet. Details siehe orders.log")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user