Woche 5
This commit is contained in:
parent
91a8776b8d
commit
091cae3b97
149
orders.log
Normal file
149
orders.log
Normal file
File diff suppressed because one or more lines are too long
0
src/u5_order_initial/order5_initial/__init__.py
Normal file
0
src/u5_order_initial/order5_initial/__init__.py
Normal file
4253
src/u5_order_initial/order5_initial/data/orders_1.json
Normal file
4253
src/u5_order_initial/order5_initial/data/orders_1.json
Normal file
File diff suppressed because it is too large
Load Diff
4253
src/u5_order_initial/order5_initial/data/orders_2.json
Normal file
4253
src/u5_order_initial/order5_initial/data/orders_2.json
Normal file
File diff suppressed because it is too large
Load Diff
4163
src/u5_order_initial/order5_initial/data/orders_3.json
Normal file
4163
src/u5_order_initial/order5_initial/data/orders_3.json
Normal file
File diff suppressed because it is too large
Load Diff
4253
src/u5_order_initial/order5_initial/data/orders_4.json
Normal file
4253
src/u5_order_initial/order5_initial/data/orders_4.json
Normal file
File diff suppressed because it is too large
Load Diff
49
src/u5_order_initial/order5_initial/load_files.py
Normal file
49
src/u5_order_initial/order5_initial/load_files.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"orders.{__name__}") # → "orders.load_files"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# JSON-File einlesen
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
def load_orders(path: str | Path) -> 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:
|
||||||
|
logger.warning("Datei nicht gefunden: %s", path)
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning(f"Konnte Datei {path} nicht decodieren")
|
||||||
|
return None
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
logger.warning(f"Datei {path} scheint ein falsches Coding zu haben")
|
||||||
|
logger.debug(
|
||||||
|
f"UnicodeDecodeError-Details: "
|
||||||
|
f"encoding={e.encoding}, "
|
||||||
|
f"reason={e.reason}, "
|
||||||
|
f"start={e.start}, "
|
||||||
|
f"end={e.end}, "
|
||||||
|
f"bad_bytes={hex(e.object[e.start])}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
46
src/u5_order_initial/order5_initial/main.py
Normal file
46
src/u5_order_initial/order5_initial/main.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils import setup_logger_extended
|
||||||
|
from load_files import load_orders
|
||||||
|
from validation import process_orders
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logger = setup_logger_extended("orders")
|
||||||
|
# logger = setup_logger()
|
||||||
|
|
||||||
|
files = [
|
||||||
|
"orders_1_valid.json",
|
||||||
|
"orders_5_non_existing_file.json",
|
||||||
|
"orders_2_parse_error.json",
|
||||||
|
"orders_3_encoding_error.json",
|
||||||
|
"orders_4_invalid_order.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)
|
||||||
|
|
||||||
|
if orders is not None:
|
||||||
|
process_orders(orders)
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"Alle {len(files)} Dateien verarbeitet. Details siehe orders.log")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
45
src/u5_order_initial/order5_initial/utils.py
Normal file
45
src/u5_order_initial/order5_initial/utils.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Logging-Konfiguration
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
115
src/u5_order_initial/order5_initial/validation.py
Normal file
115
src/u5_order_initial/order5_initial/validation.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"orders.{__name__}") # → "orders.validation"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 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>")
|
||||||
|
|
||||||
|
for item in order.get("items", []):
|
||||||
|
qty = item.get("qty")
|
||||||
|
if qty is not None and qty < 0:
|
||||||
|
raise InvalidOrderError(
|
||||||
|
order_id=order_id,
|
||||||
|
field="qty",
|
||||||
|
value=qty,
|
||||||
|
message=f"Negative Menge ({qty}) ist nicht erlaubt.",
|
||||||
|
)
|
||||||
|
|
||||||
|
total = order.get("total_chf")
|
||||||
|
if total is not None and total < 0:
|
||||||
|
raise InvalidOrderError(
|
||||||
|
order_id=order_id,
|
||||||
|
field="total_chf",
|
||||||
|
value=total,
|
||||||
|
message=f"Negativer Gesamtbetrag ({total} CHF) ist nicht erlaubt.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not order.get("order_id"):
|
||||||
|
raise InvalidOrderError(
|
||||||
|
order_id="<unbekannt>",
|
||||||
|
field="order_id",
|
||||||
|
value=order.get("order_id"),
|
||||||
|
message="Pflichtfeld 'order_id' fehlt oder ist leer.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Validierungs-Durchlauf über alle Bestellungen
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
def process_orders(orders: list[dict]) -> 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.debug("OK: %s", order.get("order_id"))
|
||||||
|
|
||||||
|
except InvalidOrderError as e:
|
||||||
|
invalid_count += 1
|
||||||
|
logger.warning(
|
||||||
|
f"Ungültige Bestellung — order_id={e.order_id}, feld={e.field}, wert={e.value}: {e.message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Validierung abgeschlossen: {valid_count} gültig, {invalid_count} ungültig."
|
||||||
|
)
|
||||||
5
src/u6_tests/pricing.py
Normal file
5
src/u6_tests/pricing.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
def discount_price(price: float, percent: float) -> float:
|
||||||
|
return price - price * percent / 100
|
||||||
|
|
||||||
|
|
||||||
|
print(discount_price(100.0, 20.0))
|
||||||
6
tests/test_pricing.py
Normal file
6
tests/test_pricing.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from src.u6_tests.pricing import discount_price
|
||||||
|
|
||||||
|
|
||||||
|
def test_discount_price_reduces_price():
|
||||||
|
result = discount_price(100.0, 20.0)
|
||||||
|
assert result == 80.0
|
||||||
Loading…
x
Reference in New Issue
Block a user