Merge pull request 'Woche 5' (#5) from errorsandloggs into master
Reviewed-on: #5
This commit is contained in:
commit
f9e7225866
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