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