diff --git a/.gamification.json b/.gamification.json index d1b45e1..59b3275 100644 --- a/.gamification.json +++ b/.gamification.json @@ -1,14 +1,17 @@ { "spieler_name": "Python-Entwicklerin", - "xp": 0, - "level": 1, - "titel": "Code-Küken 🐣", - "rang": "Bronze I", - "geloeste_kapitel": [], - "bestandene_tests": 0, + "xp": 360, + "level": 2, + "titel": "Code-Detektivin 🔍", + "rang": "Bronze II", + "geloeste_kapitel": [ + "lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools", + "lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints" + ], + "bestandene_tests": 16, "freigeschaltete_badges": [], "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T21:21:16.433076" + "zuletzt_aktualisiert": "2026-08-29T23:32:59.778016" } \ No newline at end of file diff --git a/LEHRPLAN.md b/LEHRPLAN.md index ec49768..e461195 100644 --- a/LEHRPLAN.md +++ b/LEHRPLAN.md @@ -42,6 +42,14 @@ Es gleicht die gesamte Übungsreihe (Grundlagen & Fortgeschrittene Objektori ├── 14: Desktop-GUIs mit Tkinter & MVC-Architektur (Zähler & Rechner) (Lehrer 27.0) ├── 15: Parameter (*args, **kwargs) & Eigene Container (Gaming-Inventar) (Lehrer 29.0 & 30.0) └── 16: Master-Abschlussprojekt: Professionelle Desktop-App (PetCare) (Meisterstück & MVC) + +[ 🛣️ LEHRPFAD 4: PROFESSIONAL DATA ENGINEERING & WEB-APIS ] + ├── 17: Reguläre Ausdrücke & Text-Mining (re, Muster, Quantifizierer, Gruppen) (Text & Data Mining) + ├── 18: Funktionale Programmierung (lambda, zip, enumerate, map/filter, sort) (Pythonic Code) + ├── 19: Generatoren, Iteratoren & itertools (yield, Lazy Evaluation, Streams) (Memory Efficiency) + ├── 20: Dataclasses, Type Hints & Decorators (@dataclass, typing, Wrappers) (Moderne Architektur) + ├── 21: Relationale Datenbanken & SQLite (CRUD, sqlite3, SQL-Injection Schutz) (SQL & Persistenz) + └── 22: Web-APIs, REST & JSON-Feeds (REST, JSON-Parsing, Statuscodes, Fehlerbehandlung) (Web & Daten) ``` --- @@ -79,4 +87,10 @@ Es gleicht die gesamte Übungsreihe (Grundlagen & Fortgeschrittene Objektori | **14** | **Tkinter GUI** | Echte Desktop-Apps, Widgets, `grid()`, MVC-Muster | Interaktive Zähler- & Rechner-App | | **15** | **Parameter & Container** | `*args`, `**kwargs`, Unpacking & Container-Dunder (`__getitem__`, `__setitem__`, `__delitem__`, `__contains__`, `__iter__`) | Gaming-Rucksack & Inventar | | **16** | **Master-Abschlussprojekt** | Vollständiges Desktop-Meisterstück: MVC-Architektur, Vererbung & Polymorphie, JSON-Savegames, CSV-Export, Exceptions & Tkinter-GUI | PetCare- & Tierheim-Manager Pro | +| **17** | **Reguläre Ausdrücke (re)** | Metazeichen (`\d`, `\w`, `\s`), Quantifizierer, Gruppen, `re.search`, `re.findall`, `re.sub` | E-Mail-Check, Telefon-Finder & IBAN-Maskierung | +| **18** | **Funktionale Programmierung** | `enumerate()`, `zip()`, `lambda`, `map()`, `filter()`, `sorted(..., key=...)`, `all()`, `any()` | Fließband-Datenpipeline, Kataloge & Alterssortierung | +| **19** | **Generatoren & itertools** | `yield`, Lazy Evaluation, `iter()`, `next()`, `itertools` (`chain`, `cycle`, `permutations`) | Riesige Datenströme & Speicher-Benchmarks | +| **20** | **Dataclasses & Type Hints** | `@dataclass`, `field(default_factory=...)`, Eigene Decorators, `Union`, `Callable` | Online-Shop-Architektur & Funktions-Tracker | +| **21** | **Datenbanken & SQLite** | `sqlite3`, Tabellen, CRUD, Parametrisierte Abfragen (`?`), Transaktionen | KundenDatenbank mit Kontoständen | +| **22** | **Web-APIs & JSON-Feeds** | REST-Architektur, `json.loads/dumps`, Statuscodes, defensive Fehlerbehandlung | Wetter- & Finanz-Feeds, Ticker-Generator | diff --git a/assets/certificate.js b/assets/certificate.js index 1d03f03..1d82076 100644 --- a/assets/certificate.js +++ b/assets/certificate.js @@ -33,9 +33,13 @@ titel = "Fortgeschrittenes OOP, Softwarequalität & Desktop-GUIs"; moduleDesc = "10 Module: Speicher-Referenzen, Dunder-Methoden, TDD Unit Testing, Vererbung, Polymorphie, Exceptions, JSON-Persistenz, Tkinter GUIs, Container & Masterprojekt"; trackKey = "lehrpfad_3"; + } else if (lehrpfadNummer === 4 || lehrpfadNummer === "lehrpfad_4") { + titel = "Python Professional & Data Engineering"; + moduleDesc = "6 Master-Module: Reguläre Ausdrücke (re), Functional Programming (lambda, zip, map), Generatoren & itertools, Dataclasses & Type Hints, Relationale DBs mit SQLite, REST-APIs & JSON-Feeds"; + trackKey = "lehrpfad_4"; } else if (lehrpfadNummer === "master") { - titel = "Master of Python Software Engineering"; - moduleDesc = "Vollständiger Abschluss aller 3 Lehrpfade (27 Module, 188 bestandene automatisierte Unittests, Model-View-Controller Desktop-App)"; + titel = "Grandmaster of Python Software Engineering"; + moduleDesc = "Vollständiger Abschluss aller 4 Lehrpfade (33 Module, über 220 bestandene automatisierte Unittests, SQLite-Persistenz, REST-APIs & MVC-Desktop-App)"; trackKey = "master"; } diff --git a/assets/gamification_data.js b/assets/gamification_data.js index e3667a1..c95bb89 100644 --- a/assets/gamification_data.js +++ b/assets/gamification_data.js @@ -1,17 +1,20 @@ // Automatisch generierter lokaler Gamification-Spielstand window.GAMIFICATION_DATA = { "spieler_name": "Python-Entwicklerin", - "xp": 0, - "level": 1, - "titel": "Code-Küken 🐣", - "rang": "Bronze I", - "geloeste_kapitel": [], - "bestandene_tests": 0, + "xp": 360, + "level": 2, + "titel": "Code-Detektivin 🔍", + "rang": "Bronze II", + "geloeste_kapitel": [ + "lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools", + "lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints" + ], + "bestandene_tests": 16, "freigeschaltete_badges": [], "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T21:21:16.433076" + "zuletzt_aktualisiert": "2026-08-29T23:32:59.778016" }; window.GAMIFICATION_BADGES_KATALOG = { "rechenkonegin": { diff --git a/index.html b/index.html index 47beb62..4ffcfa9 100644 --- a/index.html +++ b/index.html @@ -167,6 +167,32 @@ + + + + +
+
+
+

⚡ Lehrpfad 4: Professional Data Engineering

+
Reguläre Ausdrücke, funktionale Programmierung, Generatoren (yield/itertools) und moderne Dataclasses (17–20).
+
+ 4 Module +
+
+ +
+
+ Kapitel 17 +
🔍 Reguläre Ausdrücke (re)
+
Text-Mining & Validierung: E-Mails, Telefonnummern, Hashtags, IBAN-Maskierung & ISO-Datumsformate.
+
+ +
+
+
+ Kapitel 18 +
⚡ Funktionale Programmierung
+
Pythonic Code: Fließband-Datenpipelines mit enumerate, zip, lambda, map, filter, custom sort, any & all.
+
+ +
+
+
+ Kapitel 19 +
🌊 Generatoren & itertools
+
Memory Efficiency: Riesige Datenmengen streamen mit yield, Lazy Evaluation und itertools.
+
+ +
+
+
+ Kapitel 20 +
🏷️ Dataclasses & Type Hints
+
Moderne Architektur: Typsichere Datenmodelle mit @dataclass, Decorators und fortgeschrittenen Type Hints.
+
+ +
+
+
+
@@ -934,7 +1042,8 @@ print(mein_auto.hupen()) - + +
diff --git a/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/README.md b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/README.md new file mode 100644 index 0000000..e8dd5f5 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/README.md @@ -0,0 +1,114 @@ +# Kapitel 17: Reguläre Ausdrücke (re) – Text-Mining & Validierung 🔍🕵️ + +Willkommen zu **Kapitel 17** und dem Start von **Lehrpfad 4 (Professional Data Engineering)**! +In diesem Modul lernst du das Schweizer Taschenmesser der Textverarbeitung kennen: **Reguläre Ausdrücke (Regular Expressions / Regex)** mit Pythons eingebautem `re`-Modul. + +--- + +## 🧭 Didaktischer Hintergrund & Die Detektiv-Analogie + +In der Datenanalyse und beim Data Engineering sind 80% aller Rohdaten unstrukturiert: Logdateien, E-Mails, Rechnungs-PDFs, Social-Media-Feeds oder Kundeneingaben. Einfache String-Methoden (`.find()`, `.split()`) stoßen hier schnell an ihre Grenzen. + +> 🕵️ **Die Detektiv-Analogie:** +> Ein regulärer Ausdruck ist wie ein **Phantombild für den Muster-Detektiv**. +> Anstatt nach einem festen Text zu suchen (*"Finde exakt 'Peter'"*), suchst du nach einer **Struktur**: +> *„Suche nach 2 Buchstaben, gefolgt von 2 Ziffern, gefolgt von vielen Zahlen – das ist eine IBAN!“* + +--- + +## 🧰 1. Metazeichen: Die Bausteine von Regex + +| Metazeichen | Bedeutung | Beispiel | Treffer | +| :--- | :--- | :--- | :--- | +| `\d` | **Digit**: Beliebige Ziffer (0-9) | `\d{3}` | `123`, `999` | +| `\D` | **Keine Ziffer**: Alles außer 0-9 | `\D+` | `abc`, `Hallo` | +| `\w` | **Word character**: Buchstabe, Ziffer, Unterstrich | `\w+` | `user_42`, `Python` | +| `\W` | **Kein Word character**: Leerzeichen, Satzzeichen | `\W` | `!`, `@`, ` ` | +| `\s` | **Whitespace**: Leerzeichen, Tab, Zeilenumbruch | `\s+` | ` `, `\t`, `\n` | +| `.` | **Beliebiges Zeichen** (außer Newline) | `a.c` | `abc`, `a-c`, `a9c` | +| `^` | **Zeilenanfang / Textanfang** | `^Start` | `Starten...` | +| `$` | **Zeilenende / Textende** | `Ende$` | `...am Ende` | +| `\b` | **Wortgrenze**: Übergang von `\w` zu `\W` | `\bTag\b` | `Tag` (nicht `Tagebuch`) | + +> ⚠️ **Wichtig – Immer Raw-Strings `r"..."` nutzen:** +> Verwende für Regex-Muster in Python immer das Präfix `r"..."` (z.B. `r"\d+\s\w+"`), damit Python Backslashes nicht als Escape-Sequenzen wie `\n` oder `\t` interpretiert. + +--- + +## 🔢 2. Quantifizierer: Wie oft kommt ein Zeichen vor? + +| Quantifizierer | Häufigkeit | Bedeutung | Beispiel | +| :--- | :--- | :--- | :--- | +| `+` | $\ge 1$ | Mindestens einmal (1 bis unendlich) | `\d+` $\rightarrow$ `5`, `12345` | +| `*` | $\ge 0$ | Null oder mehrmals (optional beliebig oft) | `ab*c` $\rightarrow$ `ac`, `abc`, `abbc` | +| `?` | $0$ oder $1$ | Optional (höchstens einmal) | `https?` $\rightarrow$ `http`, `https` | +| `{n}` | Exakt $n$ | Genau $n$ Wiederholungen | `\d{4}` $\rightarrow$ `2026` | +| `{min,max}` | $min$ bis $max$ | Bereich von Wiederholungen | `\d{2,4}` $\rightarrow$ `12`, `123`, `1234` | + +--- + +## 🎯 3. Zeichenklassen & Gruppen + +### A) Benutzerdefinierte Zeichenklassen `[...]` +- `[aeiou]` $\rightarrow$ Findet einen Vokal +- `[a-zA-Z0-9]` $\rightarrow$ Alphanumerische Zeichen +- `[^0-9]` $\rightarrow$ Negation: Alles **außer** Ziffern + +### B) Gruppen `(...)` & Benannte Gruppen `(?P...)` +Mit runden Klammern schneidest du Teiltreffer gezielt heraus: +```python +import re + +muster = r"^(?P\d{4})-(?P\d{2})-(?P\d{2})$" +match = re.match(muster, "2026-08-29") + +if match: + print(match.group("jahr")) # -> '2026' + print(match.groupdict()) # -> {'jahr': '2026', 'monat': '08', 'tag': '29'} +``` + +--- + +## ⚙️ 4. Die wichtigsten Methoden des `re`-Moduls + +| Methode | Wann verwenden? | Rückgabetyp | +| :--- | :--- | :--- | +| `re.search(pattern, text)` | Erstes Vorkommen irgendwo im Text suchen | `re.Match` oder `None` | +| `re.fullmatch(pattern, text)` | Ganzen String von Anfang bis Ende prüfen (Validierung) | `re.Match` oder `None` | +| `re.findall(pattern, text)` | Alle Treffer im Text als Liste sammeln | `list[str]` oder `list[tuple]` | +| `re.finditer(pattern, text)` | Alle Treffer als Iterator mit Positionen durchlaufen | `Iterator[re.Match]` | +| `re.sub(pattern, repl, text)` | Suchen & Ersetzen (z.B. Maskierung vertraulicher Daten) | `str` | + +### Praxisbeispiel: Datenbereinigung mit `re.sub` +```python +# IBANs maskieren +text = "Konto: DE89370400440532013000" +maskiert = re.sub( + r"\b([A-Z]{2}\d{2})([A-Z0-9]{8,30})\b", + lambda m: m.group(1) + ("*" * len(m.group(2))), + text +) +print(maskiert) # -> Konto: DE89****************** +``` + +--- + +## 📁 Die Dateien in diesem Modul + +- **`index.html`**: Interaktive Lernseite mit Cheat-Sheet, Live-Beispielen, Checkliste und Lösungstipps. +- **`aufgabe.py`**: Dein Arbeitsblatt mit Type Hints und TODOs für E-Mail-Check, Telefon-Finder, Hashtags, IBAN-Maskierung und ISO-Datum-Parsing. +- **`test_aufgabe.py`**: Automatische Unittest-Suite mit Grenzfall-Tests. +- **`musterloesung.py`**: Vollständig ausprogrammierte Referenzlösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Führe die automatischen Unittests aus: +python3 test_aufgabe.py + +# 3. Oder starte aufgabe.py direkt im Terminal: +python3 aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/aufgabe.py b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/aufgabe.py new file mode 100644 index 0000000..3841a2d --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/aufgabe.py @@ -0,0 +1,222 @@ +r""" +Kapitel 17: Reguläre Ausdrücke (re) – Text-Mining & Validierung 🔍🕵️ +=================================================================== +Aufgabe: Werde zum Muster-Detektiv! Nutze Pythons eingebautes `re`-Modul, +um strukturierte Daten aus unstrukturiertem Text zu filtern, Formate zu +validieren und vertrauliche Informationen zu maskieren. + +Themen: +1. Metazeichen: \d (Ziffer), \w (Wortzeichen), \s (Whitespace), . (Beliebig), ^, $ +2. Quantifizierer: +, *, ?, {min,max} +3. Gruppen & Benannte Gruppen: (group) und (?P...) +4. Methoden: re.search(), re.match(), re.findall(), re.sub(), re.fullmatch() +""" + +import re +from typing import List, Dict, Optional, Any + + +# ============================================================================== +# TEIL 1: VALIDIERUNG VON E-MAIL-ADRESSEN +# ============================================================================== + +def ist_gueltige_email(email: str) -> bool: + r""" + TODO 1: Prüft, ob ein übergebener String eine gültige E-Mail-Adresse ist. + + Regeln für eine gültige E-Mail: + - Benutzername: Mindestens 1 Zeichen aus Buchstaben (a-z, A-Z), Ziffern (0-9), + Punkten (.), Unterstrichen (_), Plus (+) oder Bindestrichen (-). + - Ein einzelnes '@'-Trennzeichen. + - Domain-Name: Mindestens 1 Zeichen aus Buchstaben, Ziffern oder Bindestrichen. + - Domain-Endung (TLD): Ein Punkt gefolgt von mindestens 2 Buchstaben (z.B. .de, .com, .at). + - Es dürfen keine führenden/nachfolgenden Leerzeichen oder Leerzeichen innerhalb sein. + + Beispiele: + ist_gueltige_email("max.mustermann@schule.at") -> True + ist_gueltige_email("dev_123+tag@gmail.com") -> True + ist_gueltige_email("kein_at_zeichen.de") -> False + ist_gueltige_email("@domain.com") -> False + ist_gueltige_email("user@domain") -> False + ist_gueltige_email("user@domain.c") -> False + + Tipp: + Verwende ein Regex-Muster wie: r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$" + und teste mit re.fullmatch(pattern, email) is not None. + """ + # 🎯 TEILZIEL 1: Implementiere die E-Mail-Validierung + pass + + +# ============================================================================== +# TEIL 2: EXTRAKTION VON TELEFONNUMMERN +# ============================================================================== + +def finde_telefonnummern(text: str) -> List[str]: + r""" + TODO 2: Findet alle Telefonnummern in einem Text und gibt sie als Liste zurück. + + Unterstützte Formate: + - Deutsche/Österreichische Standardnummern: "0171-1234567", "0171 1234567" + - Mit Ländervorwahl: "+49 171 1234567", "+49-171-1234567", "+43 664 1234567" + - Mit Vorwahlklammern: "(089) 123456", "(030) 98765432" + - Mit Schrägstrich: "089/123456", "030/98765432" + + Beispiele: + text = "Ruf mich an unter 0171-1234567 oder im Büro unter +49 171 1234567. Notfall: (089) 123456." + finde_telefonnummern(text) + -> ["0171-1234567", "+49 171 1234567", "(089) 123456"] + + Tipp: + Nutze re.findall(). Ein typisches Suchmuster berücksichtigt: + - Ländervorwahl oder Vorwahl mit Klammern/Null: (?:\+\d{1,3}[ -]\d{2,5}|\(\d{2,5}\)|0\d{1,5}) + - Optionales Trennzeichen [- /]? + - Anschlussnummer: \d{3,10}\b + """ + # 🎯 TEILZIEL 2: Extrahiere alle Telefonnummern + pass + + +# ============================================================================== +# TEIL 3: SOCIAL MEDIA HASHTAGS EXTRAHIEREN +# ============================================================================== + +def extrahiere_hashtags(text: str) -> List[str]: + r""" + TODO 3: Extrahiere alle Social-Media-Hashtags aus einem Text. + + Regeln: + - Ein Hashtag beginnt mit dem Raute-Zeichen '#'. + - Darauf folgen 1 oder mehr Wortzeichen (Buchstaben, Ziffern, Unterstriche). + - Deutsche Umlaute (ä, ö, ü, ß) sollen ebenfalls unterstützt werden. + - Ein einzelnes '#' ohne nachfolgende Wortzeichen ist KEIN Hashtag. + + Beispiele: + extrahiere_hashtags("Lerne #python und #data_engineering! #2026 #süß") + -> ["#python", "#data_engineering", "#2026", "#süß"] + extrahiere_hashtags("Nur ein normales Zeichen # und Text") + -> [] + + Tipp: + Nutze re.findall() mit dem Muster r"#[a-zA-Z0-9_äöüÄÖÜß]+" oder r"#\w+". + """ + # 🎯 TEILZIEL 3: Extrahiere Hashtags + pass + + +# ============================================================================== +# TEIL 4: DATENSCHUTZ & MASKIERUNG (IBAN) +# ============================================================================== + +def maskiere_iban(text: str) -> str: + r""" + TODO 4: Maskiert alle IBANs in einem Text aus Datenschutzgründen. + + Regeln: + - Eine IBAN besteht aus einem 2-stelligen Ländercode (z.B. DE, AT, CH), + gefolgt von 2 Prüfziffern und weiteren 8 bis 30 alphanumerischen Zeichen. + - Die ersten 4 Zeichen (z.B. 'DE89') bleiben sichtbar. + - Alle darauffolgenden Zeichen der IBAN werden durch '*' ersetzt (exakt selbe Anzahl an Sternchen!). + - Der restliche Text um die IBAN herum bleibt unverändert. + + Beispiele: + maskiere_iban("Bitte überweisen auf DE89370400440532013000 danke!") + -> "Bitte überweisen auf DE89****************** danke!" + + maskiere_iban("AT611904300234573201 und DE1234567890") + -> "AT61**************** und DE12********" + + Tipp: + Nutze re.sub() mit einer Ersetzungsfunktion: + pattern = r"\b([A-Z]{2}\d{2})([A-Z0-9]{8,30})\b" + re.sub(pattern, lambda m: m.group(1) + ("*" * len(m.group(2))), text) + """ + # 🎯 TEILZIEL 4: Maskiere gefundene IBANs mit re.sub() + pass + + +# ============================================================================== +# TEIL 5: BENANNTE GRUPPEN (ISO-DATUM PARSEN) +# ============================================================================== + +def parse_datum_iso(datum_str: str) -> Dict[str, str]: + r""" + TODO 5: Parst ein Datum im ISO-Format (YYYY-MM-DD) mithilfe benannter Gruppen + (?P...) und gibt ein Dictionary zurück. + + Format: + - jahr: 4 Ziffern (z.B. '2026') + - monat: 2 Ziffern (z.B. '08') + - tag: 2 Ziffern (z.B. '29') + + Rückgabe: + Dict[str, str]: {'jahr': '2026', 'monat': '08', 'tag': '29'} + + Exceptions: + ValueError: Falls das Format nicht exakt dem ISO-Muster entspricht. + + Beispiele: + parse_datum_iso("2026-08-29") -> {'jahr': '2026', 'monat': '08', 'tag': '29'} + parse_datum_iso("29.08.2026") -> löst ValueError aus! + parse_datum_iso("2026/08/29") -> löst ValueError aus! + + Tipp: + Verwende das Muster r"^(?P\d{4})-(?P\d{2})-(?P\d{2})$". + Rufe match = re.fullmatch(pattern, datum_str.strip()) auf. + Mit match.groupdict() erhältst du direkt das fertige Dictionary! + """ + # 🎯 TEILZIEL 5: Parse ISO-Datum mit benannten Gruppen + pass + + +# ============================================================================== +# TEIL 6: BONUS: LOGFILE-PARSER +# ============================================================================== + +def parse_log_zeile(log_zeile: str) -> Dict[str, str]: + r""" + TODO 6 (Bonus): Parst eine Server-Logzeile und extrahiert Metadaten. + + Format einer Logzeile: + "[YYYY-MM-DD HH:MM:SS] [LEVEL] [SERVICE] NACHRICHT" + Beispiel: + "[2026-08-29 14:32:00] [ERROR] [auth_service] Login fehlgeschlagen für user 'admin'" + + Rückgabe: + Dict mit den Schlüsseln: + - "timestamp": z.B. "2026-08-29 14:32:00" + - "level": z.B. "ERROR" + - "service": z.B. "auth_service" + - "message": z.B. "Login fehlgeschlagen für user 'admin'" + + Exceptions: + ValueError: Wenn die Logzeile nicht dem erwarteten Format entspricht. + """ + # 🎯 TEILZIEL 6: Parse Server-Logzeile mit Gruppen + pass + + +if __name__ == "__main__": + print("=" * 60) + print("🔍 KAPITEL 17: REGEX-DETEKTIV TESTLAUF") + print("=" * 60) + + # 1. E-Mail Test + test_mail = "tutor@antigravity-academy.de" + print(f"E-Mail '{test_mail}' gültig?:", ist_gueltige_email(test_mail)) + + # 2. Telefonnummern + text = "Zentrale: +49 89 1234567, Mobil: 0171-9876543, Büro: (030) 555444" + print("Gefundene Telefonnummern:", finde_telefonnummern(text)) + + # 3. Hashtags + tweet = "Python 3.12 ist genial! #python #data_science #AI2026 #süß" + print("Gefundene Hashtags:", extrahiere_hashtags(tweet)) + + # 4. IBAN-Maskierung + vertraulich = "Konto A: DE89370400440532013000, Konto B: AT611904300234573201" + print("Maskierter Text:", maskiere_iban(vertraulich)) + + # 5. ISO-Datum + datum = "2026-08-29" + print("Geparstes Datum:", parse_datum_iso(datum)) diff --git a/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/index.html b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/index.html new file mode 100644 index 0000000..0d1ce85 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/index.html @@ -0,0 +1,509 @@ + + + + + + Kapitel 17: Reguläre Ausdrücke (re) – Text-Mining & Validierung + + + + +
+ + +
+
+
+ +
Kapitel 17 • Data Engineering Track
+

Reguläre Ausdrücke (re) – Text-Mining & Validierung 🔍🕵️

+

Werde zum Muster-Detektiv! Durchsuche, filtere und validiere unstrukturierte Textdaten mit Pythons mächtiger Regex-Engine.

+
+
+ +
+ + + + + +
+

1. Warum Reguläre Ausdrücke (Regex)?

+

+ Im echten Alltag eines Software-Entwicklers oder Data Engineers sind 80% aller Rohdaten unstrukturiert: E-Mails, Server-Logs, Tweets, Rechnungen oder Kundeneingaben. + Einfache String-Methoden wie .find() oder .split() reichen oft nicht aus. +

+ +
+
🕵️ Die Detektiv-Analogie mit der Suchlupe:
+

+ Ein regulärer Ausdruck ist wie ein Phantombild: + Statt nach einem festen Text zu suchen („Suche exakt nach 'Peter'“), suchst du nach einer Struktur: +

+ „Suche 2 Großbuchstaben, gefolgt von 2 Ziffern, gefolgt von weiteren Zahlen – egal welche Nummern es genau sind, das ist eine IBAN!“ +

+
+
+ + +
+

2. Cheat-Sheet: Die wichtigsten Metazeichen & Quantifizierer

+

+ In Python nutzen wir das Modul import re. Regex-Muster werden als Raw-Strings (r"...") definiert: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ZeichenKategorieBedeutungBeispiel
\dMetazeichenBeliebige Ziffer (0-9)r"\d{4}" → "2026"
\wMetazeichenWortzeichen (Buchstabe, Ziffer, Unterstrich)r"#\w+" → "#python"
\sMetazeichenWhitespace (Leerzeichen, Tab, Umbruch)r"\s+"
.MetazeichenBeliebiges Zeichen (außer Zeilenumbruch)r"a.c" → "abc", "a9c"
+QuantifiziererMindestens 1 Mal (1 bis unendlich)r"\d+" → "42"
*Quantifizierer0 oder mehrmals (optional beliebig oft)r"ab*c" → "ac", "abc"
?QuantifiziererOptional (0 oder 1 Mal)r"https?" → "http", "https"
{min,max}QuantifiziererVon min bis max Wiederholungenr"\d{2,4}"
^ und $AnkerZeilenanfang (^) und Zeilenende ($)r"^\d+$" (nur Ziffern)
+
+ + +
+

3. Die 4 Haupt-Methoden in Python

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodeZweckCode-Beispiel
re.search()Sucht den ersten Treffer irgendwo im String.m = re.search(r"\d+", "Raum 404")
re.fullmatch()Prüft, ob der gesamte String dem Muster entspricht (Validierung).re.fullmatch(r"^[a-z]+@[a-z]+\.[a-z]{2,}$", mail)
re.findall()Sammelt alle Vorkommnisse in einer Python-Liste.re.findall(r"#\w+", tweet)
re.sub()Sucht Treffer und ersetzt sie (auch per Lambda/Funktion).re.sub(r"\d", "*", "Pin: 1234") → "Pin: ****"
+ +
+
💡 Benannte Gruppen mit (?P<name>...):
+

Mit benannten Gruppen kannst du Daten direkt in ein lesbares Dictionary extrahieren:

+
+
import re
+
+pattern = r"^(?P<jahr>\d{4})-(?P<monat>\d{2})-(?P<tag>\d{2})$"
+match = re.fullmatch(pattern, "2026-08-29")
+if match:
+    print(match.groupdict())
+    # Output: {'jahr': '2026', 'monat': '08', 'tag': '29'}
+
+
+
+ + +
+

4. 📋 Deine Aufgaben-Checkliste

+

Öffne die Datei aufgabe.py und löse die 5 Teilziele:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zu TODO 4: IBAN-Maskierung mit Lambda +
+

Übergebe an re.sub() eine Funktion, die das Match-Objekt zerlegt:

+
+
pattern = r"\b([A-Z]{2}\d{2})([A-Z0-9]{8,30})\b"
+return re.sub(
+    pattern,
+    lambda m: m.group(1) + ("*" * len(m.group(2))),
+    text
+)
+
+
+
+ +
+ 🔍 Tipp zu TODO 5: ISO-Datum mit benannten Gruppen +
+

Nutze re.fullmatch und groupdict():

+
+
pattern = r"^(?P<jahr>\d{4})-(?P<monat>\d{2})-(?P<tag>\d{2})$"
+match = re.fullmatch(pattern, datum_str.strip())
+if not match:
+    raise ValueError("Ungültiges Format")
+return match.groupdict()
+
+
+
+
+ + +
+

5. 🧪 Lösung überprüfen & Testen

+ +

1. Automatische Tests ausführen:

+
+
+ Terminal (im Ordner 17_regulaere_ausdruecke_re) +
+
python3 test_aufgabe.py
+
+ +

2. Interaktives Skript starten:

+
+
+ Terminal +
+
python3 aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/musterloesung.py b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/musterloesung.py new file mode 100644 index 0000000..a202b69 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/musterloesung.py @@ -0,0 +1,77 @@ +""" +Kapitel 17: Reguläre Ausdrücke (re) – Musterlösung 💡 +==================================================== +Vollständige Referenzlösung für alle Teilaufgaben aus Kapitel 17. +""" + +import re +from typing import List, Dict, Optional, Any + + +def ist_gueltige_email(email: str) -> bool: + """ + Prüft, ob ein übergebener String eine gültige E-Mail-Adresse ist. + """ + pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$" + return bool(re.fullmatch(pattern, email.strip()) if email == email.strip() else False) + + +def finde_telefonnummern(text: str) -> List[str]: + """ + Findet alle Telefonnummern in einem Text (Standard, Vorwahl, Ländervorwahl). + """ + pattern = r"(?:\+\d{1,3}[ -]\d{2,5}|\(\d{2,5}\)|0\d{1,5})[- /]?\d{3,10}\b" + return re.findall(pattern, text) + + +def extrahiere_hashtags(text: str) -> List[str]: + """ + Extrahiert alle Social-Media-Hashtags aus einem Text. + """ + pattern = r"#[a-zA-Z0-9_äöüÄÖÜß]+" + return re.findall(pattern, text) + + +def maskiere_iban(text: str) -> str: + """ + Maskiert alle IBANs im Text: Die ersten 4 Stellen bleiben sichtbar, der Rest wird mit '*' maskiert. + """ + pattern = r"\b([A-Z]{2}\d{2})([A-Z0-9]{8,30})\b" + + def _mask_match(match: re.Match) -> str: + prefix = match.group(1) + rest = match.group(2) + return prefix + ("*" * len(rest)) + + return re.sub(pattern, _mask_match, text) + + +def parse_datum_iso(datum_str: str) -> Dict[str, str]: + """ + Parst ein Datum im ISO-Format (YYYY-MM-DD) mithilfe benannter Gruppen (?P...). + """ + pattern = r"^(?P\d{4})-(?P\d{2})-(?P\d{2})$" + match = re.fullmatch(pattern, datum_str.strip()) + if not match: + raise ValueError(f"Ungültiges ISO-Datumsformat: '{datum_str}' (erwartet: YYYY-MM-DD)") + return match.groupdict() + + +def parse_log_zeile(log_zeile: str) -> Dict[str, str]: + """ + Parst eine Server-Logzeile und extrahiert timestamp, level, service und message. + """ + pattern = r"^\[(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(?P[A-Z]+)\] \[(?P[\w-]+)\] (?P.+)$" + match = re.match(pattern, log_zeile.strip()) + if not match: + raise ValueError(f"Ungültige Logzeile: '{log_zeile}'") + return match.groupdict() + + +if __name__ == "__main__": + print("Musterlösung Kapitel 17:") + print("E-Mail validieren:", ist_gueltige_email("user.name@sub.example.com")) + print("Telefonnummern:", finde_telefonnummern("Tel: 0171-1234567 und +49 171 1234567 sowie (089) 123456")) + print("Hashtags:", extrahiere_hashtags("Lerne #python mit #code_2026! #spaß")) + print("IBAN:", maskiere_iban("DE89370400440532013000")) + print("Datum:", parse_datum_iso("2026-08-29")) diff --git a/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/test_aufgabe.py new file mode 100644 index 0000000..b92edf3 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re/test_aufgabe.py @@ -0,0 +1,179 @@ +import unittest +import sys +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel17(unittest.TestCase): + """ + Testsuite für Kapitel 17: Reguläre Ausdrücke (re) – Text-Mining & Validierung. + """ + + def setUp(self): + self.ist_gueltige_email = getattr(aufgabe, "ist_gueltige_email", None) + self.finde_telefonnummern = getattr(aufgabe, "finde_telefonnummern", None) + self.extrahiere_hashtags = getattr(aufgabe, "extrahiere_hashtags", None) + self.maskiere_iban = getattr(aufgabe, "maskiere_iban", None) + self.parse_datum_iso = getattr(aufgabe, "parse_datum_iso", None) + self.parse_log_zeile = getattr(aufgabe, "parse_log_zeile", None) + + self.assertIsNotNone(self.ist_gueltige_email, "Funktion 'ist_gueltige_email' nicht gefunden!") + self.assertIsNotNone(self.finde_telefonnummern, "Funktion 'finde_telefonnummern' nicht gefunden!") + self.assertIsNotNone(self.extrahiere_hashtags, "Funktion 'extrahiere_hashtags' nicht gefunden!") + self.assertIsNotNone(self.maskiere_iban, "Funktion 'maskiere_iban' nicht gefunden!") + self.assertIsNotNone(self.parse_datum_iso, "Funktion 'parse_datum_iso' nicht gefunden!") + + # ========================================================================== + # 1. E-Mail Validierung + # ========================================================================== + def test_01_ist_gueltige_email(self): + """Prüft E-Mail Validierung für Standard- und Grenzfälle.""" + # Gültige E-Mails + gueltige = [ + "max.mustermann@schule.at", + "anna_meier@firma.de", + "dev_123+tag@gmail.com", + "info@sub.domain.org", + "contact@company.co.uk", + "a@b.at", + "test-user@domain.io" + ] + for mail in gueltige: + self.assertTrue(self.ist_gueltige_email(mail), f"'{mail}' sollte als GÜLTIG erkannt werden!") + + # Ungültige E-Mails + ungueltige = [ + "kein_at_zeichen.de", + "@domain.com", + "name@.com", + "name@domain", + "name@domain.c", # TLD zu kurz (mind. 2 Zeichen) + "name @domain.com", # Leerzeichen im Benutzernamen + "name@ domain.com", # Leerzeichen in der Domain + "name@domain .com", # Leerzeichen vor TLD + "name@@domain.com", # Doppeltes @ + "", # Leerer String + "name@domain..com" # Doppelte Punkte + ] + for mail in ungueltige: + self.assertFalse(self.ist_gueltige_email(mail), f"'{mail}' sollte als UNGÜLTIG erkannt werden!") + + # ========================================================================== + # 2. Telefonnummern-Extraktion + # ========================================================================== + def test_02_finde_telefonnummern(self): + """Prüft das Extrahieren von Telefonnummern in verschiedenen Formaten.""" + text = ( + "Hier sind unsere Kontakte: Büro 0171-1234567 oder Zentrale +49 171 1234567. " + "In Österreich erreichst du uns unter +43 664 1234567. " + "München: (089) 123456 und Berlin: 030/98765432. " + "Keine Telefonnummer ist 123 oder das Jahr 2026." + ) + gefunden = self.finde_telefonnummern(text) + + # Sollte alle 5 Telefonnummern finden + self.assertIn("0171-1234567", gefunden) + self.assertIn("+49 171 1234567", gefunden) + self.assertIn("+43 664 1234567", gefunden) + self.assertIn("(089) 123456", gefunden) + self.assertIn("030/98765432", gefunden) + + # Grenzfall: Text ohne Nummern + self.assertEqual(self.finde_telefonnummern("Hier gibt es keine Nummern!"), []) + self.assertEqual(self.finde_telefonnummern(""), []) + + # ========================================================================== + # 3. Hashtag-Extraktion + # ========================================================================== + def test_03_extrahiere_hashtags(self): + """Prüft das Extrahieren von Hashtags inkl. Umlauten und Zahlen.""" + tweet = "Heute lernen wir #python und #data_engineering! Macht super #spaß und ist #süß. #2026 #AI_pro" + hashtags = self.extrahiere_hashtags(tweet) + + erwartet = ["#python", "#data_engineering", "#spaß", "#süß", "#2026", "#AI_pro"] + self.assertEqual(hashtags, erwartet) + + # Einzelne Rauten oder Rauten ohne Wortzeichen dürfen nicht matchen + self.assertEqual(self.extrahiere_hashtags("Nur ein # oder #! oder #+#"), []) + self.assertEqual(self.extrahiere_hashtags("Keine Hashtags vorhanden."), []) + self.assertEqual(self.extrahiere_hashtags(""), []) + + # ========================================================================== + # 4. IBAN-Maskierung + # ========================================================================== + def test_04_maskiere_iban(self): + """Prüft die datenschutzkonforme Maskierung von IBANs.""" + # Deutsche IBAN (22 Zeichen -> 4 sichtbar + 18 Sternchen) + text1 = "Konto: DE89370400440532013000 bitte prüfen." + ergebnis1 = self.maskiere_iban(text1) + self.assertEqual(ergebnis1, "Konto: DE89****************** bitte prüfen.") + + # Mehrere IBANs in einem Text + text2 = "Überweise von AT611904300234573201 an DE1234567890 sofort." + ergebnis2 = self.maskiere_iban(text2) + # AT IBAN (20 Zeichen -> 4 sichtbar + 16 Sternchen) + # DE IBAN (12 Zeichen -> 4 sichtbar + 8 Sternchen) + self.assertEqual(ergebnis2, "Überweise von AT61**************** an DE12******** sofort.") + + # Text ohne IBAN bleibt unberührt + text3 = "Keine Bankdaten hier vorhanden." + self.assertEqual(self.maskiere_iban(text3), text3) + self.assertEqual(self.maskiere_iban(""), "") + + # ========================================================================== + # 5. ISO-Datum Parsing mit benannten Gruppen + # ========================================================================== + def test_05_parse_datum_iso(self): + """Prüft das Parsen von ISO-Datumsstrings in ein Dict mit benannten Gruppen.""" + res1 = self.parse_datum_iso("2026-08-29") + self.assertEqual(res1, {"jahr": "2026", "monat": "08", "tag": "29"}) + + res2 = self.parse_datum_iso("1999-12-31") + self.assertEqual(res2, {"jahr": "1999", "monat": "12", "tag": "31"}) + + # Ungültige Formate müssen ValueError werfen + with self.assertRaises(ValueError, msg="Deutsches Datumsformat muss ValueError auslösen!"): + self.parse_datum_iso("29.08.2026") + + with self.assertRaises(ValueError, msg="Slash-Format muss ValueError auslösen!"): + self.parse_datum_iso("2026/08/29") + + with self.assertRaises(ValueError, msg="Kurzes Jahr muss ValueError auslösen!"): + self.parse_datum_iso("26-08-29") + + with self.assertRaises(ValueError, msg="Leerer String muss ValueError auslösen!"): + self.parse_datum_iso("") + + # ========================================================================== + # 6. Bonus: Logfile Parser + # ========================================================================== + def test_06_parse_log_zeile(self): + """Prüft das Parsen von Server-Logzeilen (Bonus-Funktion).""" + if self.parse_log_zeile is None: + return # Überspringen, falls Bonus nicht implementiert + + zeile = "[2026-08-29 14:32:00] [ERROR] [auth_service] Login fehlgeschlagen für user 'admin'" + try: + res = self.parse_log_zeile(zeile) + if res is not None: + self.assertEqual(res.get("timestamp"), "2026-08-29 14:32:00") + self.assertEqual(res.get("level"), "ERROR") + self.assertEqual(res.get("service"), "auth_service") + self.assertEqual(res.get("message"), "Login fehlgeschlagen für user 'admin'") + + # Ungültige Zeile + with self.assertRaises(ValueError): + self.parse_log_zeile("Keine gültige Logzeile") + except NotImplementedError: + pass + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/README.md b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/README.md new file mode 100644 index 0000000..72ea65b --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/README.md @@ -0,0 +1,152 @@ +# Kapitel 18: Funktionale Programmierung & Pythonic Code ⚡🔄 + +Willkommen zu **Kapitel 18** in **Lehrpfad 4 (Professional Data Engineering)**! +In diesem Modul lernst du, wie du mithilfe funktionaler Konzepte sauberen, ausdrucksstarken und hocheffizienten Python-Code schreibst (*Pythonic Code*). + +--- + +## 🧭 Didaktischer Hintergrund: Die Fließband-Analogie + +In der traditionellen (imperativen) Programmierung schreibst du oft lange Schleifen mit Hilfsvariablen, Indizes und Zwischenlisten: +```python +# ❌ Umständlich / Imperativ +ergebnis = [] +i = 0 +for element in elemente: + if element % 2 != 0: + ergebnis.append(element ** 2) + i += 1 +``` + +> 🏭 **Die Fließband-Analogie:** +> In der funktionalen Programmierung baust du ein **automatisiertes Fließband** auf: +> - **`filter()` (Die Prüfstation):** Lässt nur Elemente durch, die eine Bedingung erfüllen. +> - **`map()` (Die Bearbeitungsstation):** Transformiert jedes durchlaufende Teil. +> - **`zip()` (Die Montagestation):** Führt zwei parallele Bänder paarweise zusammen. +> - **`enumerate()` (Die Etikettierstation):** Versieht jedes Teil mit einer laufenden Nummer. + +--- + +## 🧰 1. Die funktionalen Werkzeuge im Überblick + +### A) `enumerate(iterable, start=0)` – Zähler ohne Hilfsvariable +```python +sprachen = ["Python", "Java", "Rust"] + +# Mit Startwert 1 nummerieren +for index, sprache in enumerate(sprachen, start=1): + print(f"{index}. {sprache}") + +# Als nummerierte Liste erzeugen: +nummeriert = [f"{i}. {s}" for i, s in enumerate(sprachen, start=1)] +# -> ['1. Python', '2. Java', '3. Rust'] +``` + +--- + +### B) `zip(*iterables)` – Paralleles Durchlaufen +```python +artikel = ["Apfel", "Banane", "Orange"] +preise = [0.99, 1.49, 1.99] + +katalog = dict(zip(artikel, preise)) +# -> {'Apfel': 0.99, 'Banane': 1.49, 'Orange': 1.99} +``` + +--- + +### C) Anonyme Lambda-Funktionen (`lambda`) +Ein `lambda` ist eine kleine, namenlose Funktion für den Einmalgebrauch: +$$\text{lambda } x: \text{ausdruck}$$ + +```python +# Normale Funktion: +def verdopple(x): + return x * 2 + +# Als Lambda-Einzeiler: +verdopple_lambda = lambda x: x * 2 +``` + +--- + +### D) `map()` & `filter()` – Die Transformations-Pipeline +```python +zahlen = [1, 2, 3, 4, 5, 6] + +# 1. Nur ungerade Zahlen herausfiltern (1, 3, 5) +ungerade = filter(lambda x: x % 2 != 0, zahlen) + +# 2. Alle verbliebenen Zahlen quadrieren (1, 9, 25) +quadriert = map(lambda x: x ** 2, ungerade) + +ergebnis = list(quadriert) # -> [1, 9, 25] +``` + +--- + +### E) Custom Sorting mit `key=lambda` +Sortiere komplexe Datenstrukturen nach beliebigen Kriterien: +```python +team = [ + {"name": "Alice", "alter": 30, "gehalt": 65000}, + {"name": "Bob", "alter": 22, "gehalt": 48000}, + {"name": "Charlie", "alter": 45, "gehalt": 80000} +] + +# Nach Alter aufsteigend sortieren +nach_alter = sorted(team, key=lambda p: p["alter"]) + +# Nach Gehalt absteigend sortieren +nach_gehalt = sorted(team, key=lambda p: p["gehalt"], reverse=True) + +# Höchstverdiener finden +chef = max(team, key=lambda p: p["gehalt"]) +``` + +--- + +### F) Kontrollfunktionen: `all()` und `any()` +```python +alter_gruppe = [20, 25, 18, 30] + +# Sind ALLE Personen mindestens 18? +print(all(alter >= 18 for alter in alter_gruppe)) # -> True + +# Gibt es MINDESTENS EINE Person unter 18? +print(any(alter < 18 for alter in alter_gruppe)) # -> False +``` + +--- + +## 📑 Vergleich: Imperativ vs. Funktional (Pythonic) + +| Anwendungsfall | Imperativ (Klassisch) | Funktional & Pythonic | +| :--- | :--- | :--- | +| **Nummerierung** | `i = 1; for x in l: print(i, x); i += 1` | `for i, x in enumerate(l, 1): print(i, x)` | +| **Paarung** | `for i in range(len(a)): dict[a[i]] = b[i]` | `dict(zip(a, b))` | +| **Transformation** | `res = []; for x in l: res.append(x*2)` | `list(map(lambda x: x*2, l))` | +| **Filterung** | `res = []; for x in l: if x>0: res.append(x)` | `list(filter(lambda x: x>0, l))` | +| **Validierung** | `ok = True; for x in l: if not cond(x): ok=False` | `all(cond(x) for x in l)` | + +--- + +## 📁 Die Dateien in diesem Modul + +- **`index.html`**: Interaktive Lernseite mit Cheat-Sheet, Code-Pipelines, Checkliste und aufklappbaren Tipps. +- **`aufgabe.py`**: Dein Arbeitsblatt mit Type Hints und TODOs für `enumerate`, `zip`, `map`/`filter`, `sorted` und `all`/`any`. +- **`test_aufgabe.py`**: Automatische Unittest-Suite zur Validierung deiner Lösungen. +- **`musterloesung.py`**: Vollständig ausprogrammierte Referenzlösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Führe die automatischen Unittests aus: +python3 test_aufgabe.py + +# 3. Oder starte aufgabe.py direkt im Terminal: +python3 aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/aufgabe.py b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/aufgabe.py new file mode 100644 index 0000000..a45e324 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/aufgabe.py @@ -0,0 +1,242 @@ +r""" +Kapitel 18: Funktionale Programmierung & Pythonic Code ⚡🔄 +========================================================= +Aufgabe: Schreibe eleganten, expressiven und hocheffizienten Python-Code! +Nutze funktionale Werkzeuge wie lambda, enumerate, zip, map, filter, +sorted(..., key=...) sowie any und all zur Datenverarbeitung. + +Themen: +1. enumerate(iterable, start=0) – Index und Wert elegant durchlaufen +2. zip(iter_a, iter_b) – Sequenzen parallel verknüpfen +3. Lambda-Funktionen – Anonyme Einzeiler (lambda x: ...) +4. map() & filter() – Transformation & Filterung auf Datenströmen +5. Custom Sorting – sorted() & max()/min() mit key=lambda ... +6. Kontrollfunktionen – any() und all() für Datenvalidierung +""" + +from typing import List, Dict, Any, Optional + + +# ============================================================================== +# TEIL 1: ENUMERATE – NUMMERIERTE LISTEN +# ============================================================================== + +def nummerierte_liste(elemente: List[str], start: int = 1) -> List[str]: + """ + TODO 1: Erstellt eine nummerierte Liste von Strings mit `enumerate()`. + + Parameter: + elemente (List[str]): Eine Liste von Zeichenketten (z.B. ["Python", "Java", "Rust"]). + start (int): Die Startnummer für die Aufzählung (Standard: 1). + + Rückgabe: + List[str]: Eine formatierte Liste wie ["1. Python", "2. Java", "3. Rust"]. + Falls `elemente` leer ist, soll eine leere Liste [] zurückgegeben werden. + + Beispiele: + nummerierte_liste(["Python", "Java", "Rust"]) + -> ["1. Python", "2. Java", "3. Rust"] + + nummerierte_liste(["A", "B"], start=0) + -> ["0. A", "1. B"] + + Tipp: + Nutze eine List Comprehension mit enumerate: + [f"{i}. {item}" for i, item in enumerate(elemente, start=start)] + """ + # 🎯 TEILZIEL 1: Implementiere nummerierte_liste mit enumerate() + pass + + +# ============================================================================== +# TEIL 2: ZIP – SEQUENZEN VERKNÜPFEN (KATALOG ERSTELLEN) +# ============================================================================== + +def kombiniere_katalog(artikel: List[str], preise: List[float]) -> Dict[str, float]: + """ + TODO 2: Verknüpft zwei getrennte Listen (Artikelnamen und Preise) mithilfe + von `zip()` zu einem Produktkatalog-Dictionary. + + Parameter: + artikel (List[str]): Liste der Artikelbezeichnungen. + preise (List[float]): Liste der entsprechenden Preise. + + Rückgabe: + Dict[str, float]: Ein Dictionary mit {artikel: preis}. + Falls eine Liste kürzer ist, schneidet `zip()` automatisch ab. + + Beispiele: + kombiniere_katalog(["Apfel", "Banane", "Orange"], [0.99, 1.49, 1.99]) + -> {"Apfel": 0.99, "Banane": 1.49, "Orange": 1.99} + + Tipp: + dict(zip(artikel, preise)) erzeugt direkt das fertige Dictionary! + """ + # 🎯 TEILZIEL 2: Kombiniere Listen mit zip() + pass + + +# ============================================================================== +# TEIL 3: MAP, FILTER & LAMBDA – DATEN PIPELINE +# ============================================================================== + +def quadriere_und_filtere(zahlen: List[int]) -> List[int]: + """ + TODO 3: Filtert alle ungeraden Zahlen aus der Eingabeliste und quadriert diese. + Nutze dafür funktionale Methoden (`filter`, `map` mit `lambda`) oder Comprehensions. + + Regeln: + - 1. Schritt (Filter): Behalte nur ungerade Zahlen (z.B. x % 2 != 0). + - 2. Schritt (Map): Berechne das Quadrat jeder ungeraden Zahl (x ** 2). + + Beispiele: + quadriere_und_filtere([1, 2, 3, 4, 5]) + -> Ungerade: [1, 3, 5] -> Quadriert: [1, 9, 25] + + quadriere_und_filtere([2, 4, 6]) + -> [] + + quadriere_und_filtere([-3, -2, 0, 3]) + -> [9, 9] + + Tipp mit funktionaler Pipeline: + ungerade = filter(lambda x: x % 2 != 0, zahlen) + quadriert = map(lambda x: x ** 2, ungerade) + return list(quadriert) + """ + # 🎯 TEILZIEL 3: Implementiere die funktionale Transformation + pass + + +# ============================================================================== +# TEIL 4: CUSTOM SORTING – SORTIEREN MIT KEY=LAMBDA +# ============================================================================== + +def sortiere_personen_nach_alter(personen: List[Dict[str, Any]], absteigend: bool = False) -> List[Dict[str, Any]]: + """ + TODO 4: Sortiert eine Liste von Personen-Dictionaries anhand des Schlüssels 'alter'. + + Parameter: + personen (List[Dict[str, Any]]): z.B. [{'name': 'Alice', 'alter': 30}, {'name': 'Bob', 'alter': 22}] + absteigend (bool): False = aufsteigend (Standard), True = absteigend (älteste zuerst). + + Rückgabe: + List[Dict[str, Any]]: Eine neue, sortierte Liste von Dictionaries. + + Beispiele: + team = [{'name': 'Alice', 'alter': 30}, {'name': 'Bob', 'alter': 22}, {'name': 'Charlie', 'alter': 45}] + sortiere_personen_nach_alter(team) + -> [{'name': 'Bob', 'alter': 22}, {'name': 'Alice', 'alter': 30}, {'name': 'Charlie', 'alter': 45}] + + Tipp: + sorted(personen, key=lambda p: p["alter"], reverse=absteigend) + """ + # 🎯 TEILZIEL 4: Sortiere mit sorted() und Lambda-Key + pass + + +# ============================================================================== +# TEIL 5: KONTROLLFUNKTIONEN ALL & ANY +# ============================================================================== + +def alle_volljaehrig(alter_liste: List[int], mindestalter: int = 18) -> bool: + """ + TODO 5: Prüft mit `all()`, ob ausnahmslos ALLE Personen in der Liste + mindestens das `mindestalter` erreicht haben. + + Parameter: + alter_liste (List[int]): Liste mit Altersangaben in Jahren. + mindestalter (int): Das geforderte Mindestalter (Standard: 18). + + Rückgabe: + bool: True wenn alle >= mindestalter (oder Liste leer ist), sonst False. + + Beispiele: + alle_volljaehrig([19, 25, 42, 18]) -> True + alle_volljaehrig([19, 17, 25, 30]) -> False (17 ist unter 18) + alle_volljaehrig([]) -> True (leere Menge erfüllt Kriterium trivial) + + Tipp: + all(alter >= mindestalter for alter in alter_liste) + """ + # 🎯 TEILZIEL 5: Prüfe Volljährigkeit mit all() + pass + + +def mindestens_ein_treffer(ergebnisse: List[bool]) -> bool: + """ + TODO 6: Prüft mit `any()`, ob mindestens ein Element in der Liste True ist. + + Parameter: + ergebnisse (List[bool]): Liste von Wahrheitswerten. + + Rückgabe: + bool: True wenn mindestens ein Element True ist, sonst False (bei leerer Liste False). + + Beispiele: + mindestens_ein_treffer([False, False, True, False]) -> True + mindestens_ein_treffer([False, False, False]) -> False + mindestens_ein_treffer([]) -> False + + Tipp: + return any(ergebnisse) + """ + # 🎯 TEILZIEL 6: Prüfe Treffer mit any() + pass + + +# ============================================================================== +# TEIL 7: BONUS: EXTREMWERTE FINDEN MIT MAX / MIN UND KEY=LAMBDA +# ============================================================================== + +def finde_extrem_produkt(produkte: List[Dict[str, Any]], modus: str = "teuerstes") -> Dict[str, Any]: + """ + TODO 7 (Bonus): Findet das teuerste oder günstigste Produkt in einer Liste. + + Parameter: + produkte (List[Dict[str, Any]]): z.B. [{'titel': 'Laptop', 'preis': 999.0}, ...] + modus (str): "teuerstes" (nutzt max()) oder "billigstes" (nutzt min()). + + Rückgabe: + Dict[str, Any]: Das Produkt-Dictionary mit dem maximalen/minimalen Preis. + + Exceptions: + ValueError: Wenn `produkte` leer ist oder `modus` weder "teuerstes" noch "billigstes" ist. + + Tipp: + max(produkte, key=lambda p: p["preis"]) bzw. min(produkte, key=lambda p: p["preis"]) + """ + # 🎯 TEILZIEL 7: Finde Extremprodukt mit max/min(..., key=lambda) + pass + + +if __name__ == "__main__": + print("=" * 60) + print("⚡ KAPITEL 18: FUNKTIONALE PROGRAMMIERUNG TESTLAUF") + print("=" * 60) + + # 1. Enumerate + sprachen = ["Python", "TypeScript", "Rust", "Go"] + print("Nummerierte Sprachen:", nummerierte_liste(sprachen)) + + # 2. Zip + artikel = ["Kaffee", "Tee", "Kuchen"] + preise = [3.50, 2.80, 4.20] + print("Katalog:", kombiniere_katalog(artikel, preise)) + + # 3. Map & Filter + zahlen = [1, 2, 3, 4, 5, 6, 7] + print("Ungerade quadriert:", quadriere_und_filtere(zahlen)) + + # 4. Custom Sorting + team = [ + {"name": "Alice", "alter": 28}, + {"name": "Bob", "alter": 22}, + {"name": "Charlie", "alter": 35} + ] + print("Team sortiert (jung -> alt):", sortiere_personen_nach_alter(team)) + + # 5. All & Any + altersgruppe = [20, 22, 19, 18] + print("Alle volljährig?:", alle_volljaehrig(altersgruppe)) + print("Mindestens ein Treffer?:", mindestens_ein_treffer([False, False, True])) diff --git a/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/index.html b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/index.html new file mode 100644 index 0000000..d484b17 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/index.html @@ -0,0 +1,449 @@ + + + + + + Kapitel 18: Funktionale Programmierung & Pythonic Code + + + + +
+ + +
+
+
+ +
Kapitel 18 • Data Engineering Track
+

Funktionale Programmierung & Pythonic Code ⚡🔄

+

Schreibe eleganten, kompakten und hocheffizienten Python-Code mit Lambda, enumerate, zip, map, filter, custom sort und Kontrollfunktionen.

+
+
+ +
+ + + + + +
+

1. Warum funktionale Idiome in Python?

+

+ In Python gibt es oft zwei Wege, eine Aufgabe zu lösen: Den langen, imperativen Weg mit vielen Hilfsvariablen und verschachtelten Schleifen – oder den eleganten, funktionalen Weg (Pythonic Code). +

+ +
+
🏭 Die automatisierte Fließband-Analogie:
+

+ Stelle dir eine moderne Fertigungsstraße vor: +

+
    +
  • enumerate() (Die Etikettierstation): Klebt an jedes Teil automatisch die aktuelle Positionsnummer.
  • +
  • zip() (Die Fügestation): Bringt Bauteile von zwei parallelen Fließbändern paarweise zusammen.
  • +
  • filter() (Die Qualitätsweiche): Lässt nur Bauteile passieren, die ein Prüfkriterium erfüllen.
  • +
  • map() (Die Veredelungsstation): Wendet eine Transformation auf jedes durchlaufende Teil an.
  • +
  • all() / any() (Die Sensorüberwachung): Meldet sofort, ob alle Teile fehlerfrei sind oder mindestens ein Alarm anliegt.
  • +
+
+
+ + +
+

2. Cheat-Sheet: Funktionale Idiome auf einen Blick

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Funktion / IdiomZweckTypisches Code-BeispielErgebnis
enumerate(l, start)Iteriert gleichzeitig über Index und Element.[f"{i}. {x}" for i, x in enumerate(["A","B"], 1)]['1. A', '2. B']
zip(a, b)Verknüpft mehrere Sequenzen paarweise.dict(zip(["Name", "Alter"], ["Max", 25])){'Name': 'Max', 'Alter': 25}
lambda x: ...Anonyme Mini-Funktion als Einzeiler.verdoppeln = lambda x: x * 2verdoppeln(5) → 10
map(f, iter)Wendet Funktion f auf alle Elemente an.list(map(lambda x: x**2, [1, 2, 3]))[1, 4, 9]
filter(p, iter)Filtert Elemente nach Prädikat p.list(filter(lambda x: x > 0, [-2, 0, 5]))[5]
sorted(..., key=...)Sortiert nach beliebigem Schlüssel (Lambda).sorted(user, key=lambda u: u["alter"])Sortierte Liste
all(iter)Gibt True, wenn ALLE Elemente wahr sind.all(x >= 18 for x in [20, 25, 18])True
any(iter)Gibt True, wenn MINDESTENS EIN Element wahr ist.any(x == 0 for x in [1, 2, 0, 4])True
+
+ + +
+

3. 📋 Deine Aufgaben-Checkliste

+

Öffne die Datei aufgabe.py und löse die 6 Kern-Aufgaben:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zu TODO 3: Map & Filter verketten +
+

Du kannst filter() direkt in map() schachteln und mit list() materialisieren:

+
+
return list(map(lambda x: x ** 2, filter(lambda x: x % 2 != 0, zahlen)))
+
+
+
+ +
+ 🔍 Tipp zu TODO 4: Sortieren mit Lambda-Key +
+

Nutze den key-Parameter von sorted():

+
+
return sorted(personen, key=lambda p: p["alter"], reverse=absteigend)
+
+
+
+
+ + +
+

4. 🧪 Lösung überprüfen & Testen

+ +

1. Automatische Tests ausführen:

+
+
+ Terminal (im Ordner 18_funktionale_programmierung_lambda) +
+
python3 test_aufgabe.py
+
+ +

2. Interaktives Skript starten:

+
+
+ Terminal +
+
python3 aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/musterloesung.py b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/musterloesung.py new file mode 100644 index 0000000..fa6a59b --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/musterloesung.py @@ -0,0 +1,76 @@ +""" +Kapitel 18: Funktionale Programmierung & Pythonic Code – Musterlösung 💡 +======================================================================== +Vollständige Referenzlösung für alle Teilaufgaben aus Kapitel 18. +""" + +from typing import List, Dict, Any, Optional + + +def nummerierte_liste(elemente: List[str], start: int = 1) -> List[str]: + """ + Erstellt eine nummerierte Liste von Strings mit enumerate(). + """ + return [f"{i}. {item}" for i, item in enumerate(elemente, start=start)] + + +def kombiniere_katalog(artikel: List[str], preise: List[float]) -> Dict[str, float]: + """ + Verknüpft Artikelnamen und Preise mittels zip() zu einem Dictionary. + """ + return dict(zip(artikel, preise)) + + +def quadriere_und_filtere(zahlen: List[int]) -> List[int]: + """ + Filtert ungerade Zahlen und quadriert sie funktional mit map() und filter(). + """ + return list(map(lambda x: x ** 2, filter(lambda x: x % 2 != 0, zahlen))) + + +def sortiere_personen_nach_alter(personen: List[Dict[str, Any]], absteigend: bool = False) -> List[Dict[str, Any]]: + """ + Sortiert eine Liste von Personen-Dictionaries anhand von 'alter' mit sorted() und key=lambda. + """ + return sorted(personen, key=lambda p: p["alter"], reverse=absteigend) + + +def alle_volljaehrig(alter_liste: List[int], mindestalter: int = 18) -> bool: + """ + Prüft mit all(), ob alle Personen in der Liste mindestens mindestalter Jahre alt sind. + """ + return all(alter >= mindestalter for alter in alter_liste) + + +def mindestens_ein_treffer(ergebnisse: List[bool]) -> bool: + """ + Prüft mit any(), ob mindestens ein Element True ist. + """ + return any(ergebnisse) + + +def finde_extrem_produkt(produkte: List[Dict[str, Any]], modus: str = "teuerstes") -> Dict[str, Any]: + """ + Findet das teuerste oder günstigste Produkt mit max() / min() und key=lambda. + """ + if not produkte: + raise ValueError("Produktliste darf nicht leer sein!") + if modus == "teuerstes": + return max(produkte, key=lambda p: p["preis"]) + elif modus == "billigstes": + return min(produkte, key=lambda p: p["preis"]) + else: + raise ValueError(f"Ungültiger Modus '{modus}'. Erlaubt sind 'teuerstes' und 'billigstes'.") + + +if __name__ == "__main__": + print("Musterlösung Kapitel 18:") + print("1. Enumerate:", nummerierte_liste(["Python", "Rust", "Go"])) + print("2. Zip:", kombiniere_katalog(["Kaffee", "Kuchen"], [3.5, 4.2])) + print("3. Map/Filter:", quadriere_und_filtere([1, 2, 3, 4, 5])) + team = [{"name": "A", "alter": 30}, {"name": "B", "alter": 20}] + print("4. Custom Sort:", sortiere_personen_nach_alter(team)) + print("5. all():", alle_volljaehrig([20, 21, 18])) + print("6. any():", mindestens_ein_treffer([False, True])) + items = [{"titel": "A", "preis": 10}, {"titel": "B", "preis": 50}] + print("7. Extremwert:", finde_extrem_produkt(items, "teuerstes")) diff --git a/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/test_aufgabe.py new file mode 100644 index 0000000..20169d5 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda/test_aufgabe.py @@ -0,0 +1,182 @@ +import unittest +import sys +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel18(unittest.TestCase): + """ + Testsuite für Kapitel 18: Funktionale Programmierung & Pythonic Code. + """ + + def setUp(self): + self.nummerierte_liste = getattr(aufgabe, "nummerierte_liste", None) + self.kombiniere_katalog = getattr(aufgabe, "kombiniere_katalog", None) + self.quadriere_und_filtere = getattr(aufgabe, "quadriere_und_filtere", None) + self.sortiere_personen_nach_alter = getattr(aufgabe, "sortiere_personen_nach_alter", None) + self.alle_volljaehrig = getattr(aufgabe, "alle_volljaehrig", None) + self.mindestens_ein_treffer = getattr(aufgabe, "mindestens_ein_treffer", None) + self.finde_extrem_produkt = getattr(aufgabe, "finde_extrem_produkt", None) + + self.assertIsNotNone(self.nummerierte_liste, "Funktion 'nummerierte_liste' nicht gefunden!") + self.assertIsNotNone(self.kombiniere_katalog, "Funktion 'kombiniere_katalog' nicht gefunden!") + self.assertIsNotNone(self.quadriere_und_filtere, "Funktion 'quadriere_und_filtere' nicht gefunden!") + self.assertIsNotNone(self.sortiere_personen_nach_alter, "Funktion 'sortiere_personen_nach_alter' nicht gefunden!") + self.assertIsNotNone(self.alle_volljaehrig, "Funktion 'alle_volljaehrig' nicht gefunden!") + self.assertIsNotNone(self.mindestens_ein_treffer, "Funktion 'mindestens_ein_treffer' nicht gefunden!") + + # ========================================================================== + # 1. Enumerate – Nummerierte Listen + # ========================================================================== + def test_01_nummerierte_liste(self): + """Prüft enumerate() mit Standardstartwert 1 und benutzerdefiniertem Start.""" + sprachen = ["Python", "Java", "Rust"] + ergebnis = self.nummerierte_liste(sprachen) + self.assertEqual(ergebnis, ["1. Python", "2. Java", "3. Rust"]) + + # Startwert 0 + ergebnis_0 = self.nummerierte_liste(["A", "B", "C"], start=0) + self.assertEqual(ergebnis_0, ["0. A", "1. B", "2. C"]) + + # Startwert 10 + ergebnis_10 = self.nummerierte_liste(["Erster", "Zweiter"], start=10) + self.assertEqual(ergebnis_10, ["10. Erster", "11. Zweiter"]) + + # Leere Liste + self.assertEqual(self.nummerierte_liste([]), []) + + # ========================================================================== + # 2. Zip – Katalog-Kombination + # ========================================================================== + def test_02_kombiniere_katalog(self): + """Prüft zip() zur Kombination von Artikeln und Preisen.""" + artikel = ["Apfel", "Banane", "Orange"] + preise = [0.99, 1.49, 1.99] + katalog = self.kombiniere_katalog(artikel, preise) + self.assertEqual(katalog, {"Apfel": 0.99, "Banane": 1.49, "Orange": 1.99}) + + # Unterschiedliche Längen (zip schneidet ab) + katalog_kurz = self.kombiniere_katalog(["Kaffee", "Tee"], [3.50, 2.80, 9.99]) + self.assertEqual(katalog_kurz, {"Kaffee": 3.50, "Tee": 2.80}) + + # Leere Listen + self.assertEqual(self.kombiniere_katalog([], []), {}) + + # ========================================================================== + # 3. Map & Filter & Lambda – Pipeline + # ========================================================================== + def test_03_quadriere_und_filtere(self): + """Prüft Filterung ungerader Zahlen und deren Quadrierung.""" + # Gemischte Zahlen + zahlen = [1, 2, 3, 4, 5, 6, 7] + ergebnis = self.quadriere_und_filtere(zahlen) + self.assertEqual(ergebnis, [1, 9, 25, 49]) + + # Nur gerade Zahlen -> Leeres Ergebnis + self.assertEqual(self.quadriere_und_filtere([2, 4, 6, 8]), []) + + # Negative ungerade Zahlen: (-3)^2 = 9 + self.assertEqual(self.quadriere_und_filtere([-3, -2, -1, 0, 1]), [9, 1, 1]) + + # Leere Liste + self.assertEqual(self.quadriere_und_filtere([]), []) + + # ========================================================================== + # 4. Custom Sorting mit Key=Lambda + # ========================================================================== + def test_04_sortiere_personen_nach_alter(self): + """Prüft benutzerdefiniertes Sortieren nach Alter (aufsteigend & absteigend).""" + personen = [ + {"name": "Charlie", "alter": 45}, + {"name": "Alice", "alter": 30}, + {"name": "Bob", "alter": 22}, + {"name": "Dora", "alter": 18} + ] + + # Aufsteigend + sortiert_auf = self.sortiere_personen_nach_alter(personen, absteigend=False) + namen_auf = [p["name"] for p in sortiert_auf] + self.assertEqual(namen_auf, ["Dora", "Bob", "Alice", "Charlie"]) + + # Absteigend + sortiert_ab = self.sortiere_personen_nach_alter(personen, absteigend=True) + namen_ab = [p["name"] for p in sortiert_ab] + self.assertEqual(namen_ab, ["Charlie", "Alice", "Bob", "Dora"]) + + # Original-Liste darf nicht verändert werden (neue Liste zurückgeben) + self.assertIsNot(sortiert_auf, personen) + + # ========================================================================== + # 5. all() – Volljährigkeit + # ========================================================================== + def test_05_alle_volljaehrig(self): + """Prüft all() mit Standardmindestalter 18 und dynamischem Mindestalter.""" + # Alle >= 18 + self.assertTrue(self.alle_volljaehrig([18, 19, 25, 50])) + + # Mindestens einer < 18 + self.assertFalse(self.alle_volljaehrig([18, 17, 25, 50])) + self.assertFalse(self.alle_volljaehrig([0])) + + # Anderes Mindestalter (z.B. 21) + self.assertTrue(self.alle_volljaehrig([21, 25, 30], mindestalter=21)) + self.assertFalse(self.alle_volljaehrig([20, 25, 30], mindestalter=21)) + + # Leere Liste ist trivial True in Python + self.assertTrue(self.alle_volljaehrig([])) + + # ========================================================================== + # 6. any() – Mindestens ein Treffer + # ========================================================================== + def test_06_mindestens_ein_treffer(self): + """Prüft any() auf Wahrheitswerte.""" + self.assertTrue(self.mindestens_ein_treffer([False, False, True, False])) + self.assertTrue(self.mindestens_ein_treffer([True, True, True])) + self.assertFalse(self.mindestens_ein_treffer([False, False, False])) + self.assertFalse(self.mindestens_ein_treffer([])) + + # ========================================================================== + # 7. Bonus: Extremwert-Finder + # ========================================================================== + def test_07_finde_extrem_produkt(self): + """Prüft das Finden von Extremwerten mit max/min und key=lambda.""" + if self.finde_extrem_produkt is None: + return + + produkte = [ + {"titel": "Kugelschreiber", "preis": 2.50}, + {"titel": "Laptop", "preis": 1299.00}, + {"titel": "Kaffeetasse", "preis": 8.90}, + {"titel": "Monitor", "preis": 349.00} + ] + + try: + teuerstes = self.finde_extrem_produkt(produkte, modus="teuerstes") + if teuerstes is not None: + self.assertEqual(teuerstes["titel"], "Laptop") + self.assertEqual(teuerstes["preis"], 1299.00) + + billigstes = self.finde_extrem_produkt(produkte, modus="billigstes") + if billigstes is not None: + self.assertEqual(billigstes["titel"], "Kugelschreiber") + self.assertEqual(billigstes["preis"], 2.50) + + # Fehlerfälle + with self.assertRaises(ValueError): + self.finde_extrem_produkt([], modus="teuerstes") + + with self.assertRaises(ValueError): + self.finde_extrem_produkt(produkte, modus="ungueltig") + except NotImplementedError: + pass + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/README.md b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/README.md new file mode 100644 index 0000000..0b98003 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/README.md @@ -0,0 +1,147 @@ +# Kapitel 19: Generatoren, Iteratoren & itertools (Memory Efficiency & Streaming) 🌊⚡ + +Willkommen zu **Kapitel 19** im Lehrpfad 4 (*Professional Data Engineering*)! +In diesem Modul lernst du, wie professionelle Software-Entwickler riesige Datenmengen speichereffizient im RAM verarbeiten. Statt Gigabytes an Daten auf einmal in eine Liste zu laden und dein System zum Absturz zu bringen, streamst du Werte **on-demand** Tropfen für Tropfen mit **Generatoren** und dem mächtigen **`itertools`**-Modul. + +--- + +## 🧭 Didaktischer Hintergrund + +In Data Pipelines, Web-Servern und Big-Data-Anwendungen stößt klassisches Laden von Daten schnell an physikalische RAM-Grenzen: +- **Speicherüberlauf (Out of Memory):** Eine Liste mit 50 Millionen Objekten belegt mehrere Gigabytes Arbeitsspeicher. +- **Lazy Evaluation (Verzögerte Auswertung):** Werte werden erst in dem exakten Mikrosekunden-Moment berechnet, in dem sie gebraucht werden. +- **Null RAM-Verschwendung:** Ein Generator verbraucht unabhängig davon, ob er 10 oder 10 Milliarden Elemente liefert, immer nur eine winzige, konstante Speichermenge (ca. 100 Bytes)! + +--- + +## 🚰 1. Die Badewannen- vs. Wasserhahn-Analogie + +``` +🔴 DIE BADEWANNE (Klassische Listen & Arrays) +┌────────────────────────────────────────────────────────┐ +│ [Wert 1, Wert 2, Wert 3, ..., Wert 10.000.000] │ +│ -> 800 MB RAM belegt! Alles muss VORHER im RAM sein! │ +└────────────────────────────────────────────────────────┘ + +🟢 DER WASSERHAHN (Generatoren & Streams mit yield) +┌────────────────────────────────────────────────────────┐ +│ [Wasserhahn] ── Tropfen 1 ──> [Verarbeitung] │ +│ ── Tropfen 2 ──> [Verarbeitung] │ +│ ── Tropfen 3 ──> [Verarbeitung] │ +│ -> ~100 Bytes RAM! Jeder Wert wird nach Gebrauch │ +│ wieder verworfen oder weitergeleitet. │ +└────────────────────────────────────────────────────────┘ +``` + +--- + +## 🔄 2. Iterables vs. Iterators (`iter()`, `next()`, `StopIteration`) + +In Python gibt es einen fundamentalen Unterschied: + +| Begriff | Was ist das? | Wie wird es erzeugt? | Typische Beispiele | +| :--- | :--- | :--- | :--- | +| **Iterable** | Eine Sammlung, über die man iterieren kann (besitzt `__iter__()`). | Listen, Tupel, Strings, Dicts | `[1, 2, 3]`, `"Hallo"` | +| **Iterator** | Das Zustandsobjekt, das den aktuellen Zeiger hält und mit `next()` den nächsten Wert liefert (besitzt `__next__()`). | `it = iter(iterable)` | `iter([1, 2, 3])` | + +### Was passiert bei `for x in sammlung:` unter der Haube? +1. Python ruft `it = iter(sammlung)` auf, um einen Iterator zu erhalten. +2. In jeder Schleifenrunde wird `x = next(it)` aufgerufen. +3. Wenn keine Elemente mehr vorhanden sind, löst Python intern die `StopIteration`-Exception aus und beendet die Schleife sauber. + +```python +daten = [10, 20, 30] +it = iter(daten) + +print(next(it)) # -> 10 +print(next(it)) # -> 20 +print(next(it)) # -> 30 +# print(next(it)) # -> Löst StopIteration aus! +``` + +--- + +## 🪄 3. Eigene Generatoren mit `yield` + +Das Schlüsselwort **`yield`** verwandelt eine gewöhnliche Funktion in eine Generator-Funktion. +- Ein `return` beendet die Funktion und vernichtet alle lokalen Variablen. +- Ein `yield` pausiert die Funktion, gibt den Zwischenwert zurück und merkt sich den exakten Programmzustand (Zeile, lokale Variablen), um beim nächsten `next()` genau dort weiterzumachen! + +```python +def mein_generator(): + print("Starte Schritt 1...") + yield "Erster Wert" + print("Mache weiter bei Schritt 2...") + yield "Zweiter Wert" + print("Fertig!") + +gen = mein_generator() # Noch kein Code wird ausgeführt! +print(next(gen)) # Druckt: Starte Schritt 1... -> Erster Wert +print(next(gen)) # Druckt: Mache weiter... -> Zweiter Wert +``` + +### Unendliche Generatoren: +Mit `yield` sind unendliche Sequenzen möglich, ohne dass der Speicher überläuft: + +```python +def endlos_zaehler(start=0, schritt=1): + aktuell = start + while True: + yield aktuell + aktuell += schritt +``` + +--- + +## ⚡ 4. Generator-Expressions: `(x**2 for x in ...)` + +Genauso wie es List Comprehensions `[x for x in ...]` gibt, kannst du mit runden Klammern `(x for x in ...)` eine schlanke **Generator Expression** erzeugen: + +```python +# 🔴 List Comprehension: Erzeugt 10 Mio. Zahlen sofort im RAM (ca. 80 MB!) +quadrate_liste = [x**2 for x in range(10_000_000)] + +# 🟢 Generator Expression: Kostet ca. 100 Bytes RAM! +quadrate_gen = (x**2 for x in range(10_000_000)) + +print(next(quadrate_gen)) # 0 +print(next(quadrate_gen)) # 1 +print(next(quadrate_gen)) # 4 +``` + +--- + +## 🧰 5. Das `itertools`-Modul (Die High-Performance Toolbox) + +Pythons Standardbibliothek liefert mit `import itertools` hochgradig optimierte C-Iteratoren: + +| Funktion | Erklärung | Code-Beispiel | +| :--- | :--- | :--- | +| `itertools.chain(*iterables)` | Hängt mehrere Iterables lückenlos hintereinander | `chain([1, 2], [3, 4])` $\rightarrow$ `1, 2, 3, 4` | +| `itertools.cycle(iterable)` | Wiederholt ein Iterable in einer Endlosschleife | `cycle(['A', 'B'])` $\rightarrow$ `A, B, A, B, A...` | +| `itertools.count(start, step)` | Unendlicher Zähler | `count(10, 2)` $\rightarrow$ `10, 12, 14, 16...` | +| `itertools.permutations(p, r)` | Alle geordneten Anordnungen der Länge $r$ | `permutations('AB', 2)` $\rightarrow$ `('A','B'), ('B','A')` | +| `itertools.combinations(p, r)` | Alle ungeordneten Kombinationen ohne Duplikate | `combinations('ABC', 2)` $\rightarrow$ `('A','B'), ('A','C'), ('B','C')` | +| `itertools.islice(iterable, n)`| Schneidet die ersten $n$ Elemente aus einem Stream | `islice(count(), 5)` $\rightarrow$ `0, 1, 2, 3, 4` | + +--- + +## 📁 Die Dateien in diesem Ordner + +- **`index.html`**: Interaktive Web-Lernseite mit visuellen Badewannen-Diagrammen, Code-Vergleichen, Cheat-Sheets und Checkliste. +- **`aufgabe.py`**: Dein Arbeitsblatt mit Type Hints und didaktischen TODOs für Generatoren, Filter und `itertools`. +- **`test_aufgabe.py`**: Automatische Unittest-Suite zur Überprüfung aller Anforderungen. +- **`musterloesung.py`**: Vollständig ausprogrammierte Referenzlösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Führe die automatischen Tests aus: +python3 test_aufgabe.py + +# 3. Oder starte das interaktive Benchmark-Skript: +python3 aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/aufgabe.py b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/aufgabe.py new file mode 100644 index 0000000..f9963f3 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/aufgabe.py @@ -0,0 +1,271 @@ +""" +Kapitel 19: Generatoren, Iteratoren & itertools (Memory Efficiency & Streaming) 🌊⚡ +================================================================================== +Lerne speichereffiziente Datenverarbeitung in Python: +1. Iterables vs. Iterators (iter(), next(), StopIteration) +2. Eigene Generatoren mit 'yield' (Werte on-demand generieren, 0 RAM-Verschwendung) +3. Generator-Expressions (x**2 for x in riesige_daten) +4. Das itertools-Modul (chain, cycle, count, combinations, permutations) + +Didaktische Analogie: +- "Gefüllte Badewanne" (Listen): Alle Daten liegen gleichzeitig im RAM. +- "Wasserhahn" (Generatoren): Daten fließen Tropfen für Tropfen, genau dann wenn sie gebraucht werden. +""" + +import sys +import itertools +from typing import Iterator, Iterable, Any, Union, List, Tuple + + +# ============================================================================== +# TEIL 1: GRUNDLAGEN VON ITERATOREN & GENERATOREN MIT YIELD +# ============================================================================== + +def endlos_zaehler(start: int = 0, schritt: int = 1) -> Iterator[int]: + """ + Ein unendlicher Generator, der Zahlen beginnend bei 'start' mit der + Schrittweite 'schritt' generiert. + + Parameter: + start (int): Die Startzahl (Standard: 0). + schritt (int): Die Schrittweite (Standard: 1). + + Erzeugt (yield): + int: Die nächste Zahl in der unendlichen Sequenz. + + Beispiele: + z = endlos_zaehler(10, 2) + next(z) -> 10 + next(z) -> 12 + next(z) -> 14 + """ + # ========================================================================== + # 🎯 TEILZIEL 1 (TODO 1): Implementiere endlos_zaehler + # 1. Initialisiere eine Variable 'aktuell' mit dem Wert 'start'. + # 2. Nutze eine unendliche Schleife (while True:). + # 3. Gib 'aktuell' mit 'yield' an den Aufrufer zurück. + # 4. Erhöhe 'aktuell' um 'schritt'. + # ========================================================================== + pass + + +def fibonacci_generator(anzahl: int) -> Iterator[int]: + """ + Ein Generator, der die ersten 'anzahl' Fibonacci-Zahlen on-demand erzeugt. + Die Fibonacci-Folge beginnt mit: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... + + Parameter: + anzahl (int): Wie viele Fibonacci-Zahlen erzeugt werden sollen (>= 0). + + Erzeugt (yield): + int: Die jeweils nächste Fibonacci-Zahl. + + Exceptions: + ValueError: Falls 'anzahl' negativ (< 0) ist. + + Beispiele: + list(fibonacci_generator(5)) -> [0, 1, 1, 2, 3] + list(fibonacci_generator(0)) -> [] + """ + # ========================================================================== + # 🎯 TEILZIEL 2 (TODO 2): Implementiere fibonacci_generator + # 1. Validiere 'anzahl': Wenn anzahl < 0, löse ValueError aus. + # 2. Setze die Startwerte a = 0, b = 1. + # 3. Wiederhole in einer Schleife 'anzahl'-mal: + # - Gib 'a' mit 'yield' zurück. + # - Aktualisiere die Werte mit Tupel-Zuweisung: a, b = b, a + b + # ========================================================================== + pass + + +# ============================================================================== +# TEIL 2: SPEICHERSCHONENDE FILTER & GENERATOR-EXPRESSIONS +# ============================================================================== + +def filter_grosse_zahlen(zahlen_iterable: Iterable[Union[int, float]], schwellenwert: float) -> Iterator[Union[int, float]]: + """ + Ein Generator-Filter, der nur Zahlen liefert, die strikt größer als + der angegebene 'schwellenwert' sind (x > schwellenwert). + + Wichtig: Es darf KEINE Zwischenliste im Speicher aufgebaut werden! + Die Werte müssen einzeln gestreamt werden. + + Parameter: + zahlen_iterable (Iterable): Eine beliebige Sequenz oder ein Generator von Zahlen. + schwellenwert (float): Der Grenzwert. + + Erzeugt (yield): + Union[int, float]: Jede Zahl aus zahlen_iterable, die > schwellenwert ist. + + Beispiele: + daten = [10, 5, 20, 3, 50] + list(filter_grosse_zahlen(daten, 15)) -> [20, 50] + """ + # ========================================================================== + # 🎯 TEILZIEL 3 (TODO 3): Implementiere filter_grosse_zahlen + # 1. Iteriere mit einer for-Schleife über jedes Element in 'zahlen_iterable'. + # 2. Prüfe, ob das Element > schwellenwert ist. + # 3. Wenn ja, gib es mit 'yield' weiter. + # ========================================================================== + pass + + +def erzeuge_quadrat_generator(n: int) -> Iterator[int]: + """ + Erzeugt eine Generator-Expression für Quadratzahlen von 1 bis n (inklusive). + + Parameter: + n (int): Die Obergrenze (>= 0). + + Rückgabe: + Iterator[int]: Ein Generator-Objekt (x**2 for x in ...), keine Liste! + + Exceptions: + ValueError: Falls n < 0 ist. + + Beispiele: + gen = erzeuge_quadrat_generator(4) + list(gen) -> [1, 4, 9, 16] + """ + # ========================================================================== + # 🎯 TEILZIEL 4 (TODO 4): Implementiere erzeuge_quadrat_generator + # 1. Wenn n < 0, löse einen ValueError aus. + # 2. Gib direkt eine Generator-Expression zurück: (x**2 for x in range(1, n + 1)) + # ========================================================================== + pass + + +# ============================================================================== +# TEIL 3: DAS ITERTOOLS-MODUL (CHAIN, PERMUTATIONS & CYCLE) +# ============================================================================== + +def kette_listen(*listen: Iterable[Any]) -> List[Any]: + """ + Verbindet beliebig viele Iterables (*args) mit 'itertools.chain' zu einer + einzigen flachen Liste. + + Parameter: + *listen (Iterable[Any]): Beliebig viele Iterables (Listen, Tupel, Generatoren). + + Rückgabe: + List[Any]: Eine zusammenhängende Liste aller Elemente. + + Beispiele: + kette_listen([1, 2], [3, 4], [5]) -> [1, 2, 3, 4, 5] + kette_listen([], [10]) -> [10] + """ + # ========================================================================== + # 🎯 TEILZIEL 5 (TODO 5): Implementiere kette_listen mit itertools.chain + # 1. Nutze itertools.chain(*listen) oder itertools.chain.from_iterable(listen). + # 2. Wandle das Ergebnis in eine normale Python-Liste um (list(...)) und gib sie zurück. + # ========================================================================== + pass + + +def erzeuge_passwort_kombinationen(zeichen: str, laenge: int) -> List[str]: + """ + Erzeugt alle möglichen Permutationen der angegebenen Zeichenkette mit der + exakten Länge 'laenge' unter Verwendung von 'itertools.permutations'. + + Parameter: + zeichen (str): Die verfügbaren Zeichen als String (z.B. "abc"). + laenge (int): Die Länge der gesuchten Kombinationen. + + Rückgabe: + List[str]: Eine Liste von Strings mit allen Permutationen. + + Exceptions: + ValueError: Falls laenge < 0 oder laenge > len(zeichen) ist. + + Beispiele: + erzeuge_passwort_kombinationen("abc", 2) -> ['ab', 'ac', 'ba', 'bc', 'ca', 'cb'] + erzeuge_passwort_kombinationen("ab", 1) -> ['a', 'b'] + """ + # ========================================================================== + # 🎯 TEILZIEL 6 (TODO 6): Implementiere erzeuge_passwort_kombinationen + # 1. Validiere: Wenn laenge < 0 oder laenge > len(zeichen), löse ValueError aus. + # 2. Verwende itertools.permutations(zeichen, laenge). + # 3. Führe jedes generierte Tupel von Zeichen mit "".join(...) zu einem String zusammen. + # 4. Gib alle kombinierten Strings als Liste zurück. + # ========================================================================== + pass + + +def zyklische_elemente(elemente: List[Any], anzahl: int) -> List[Any]: + """ + Nimmt eine Liste von Elementen und liefert die ersten 'anzahl' Elemente + in zyklischer Reihenfolge mit 'itertools.cycle'. + + Parameter: + elemente (List[Any]): Die Ausgangsliste (darf nicht leer sein, falls anzahl > 0). + anzahl (int): Wie viele Elemente zyklisch entnommen werden sollen. + + Rückgabe: + List[Any]: Liste mit 'anzahl' Elementen. + + Exceptions: + ValueError: Falls anzahl < 0 ist oder falls elemente leer ist bei anzahl > 0. + + Beispiele: + zyklische_elemente(["Rot", "Gruen", "Blau"], 7) + -> ["Rot", "Gruen", "Blau", "Rot", "Gruen", "Blau", "Rot"] + """ + # ========================================================================== + # 🎯 TEILZIEL 7 (TODO 7): Implementiere zyklische_elemente + # 1. Validiere anzahl >= 0 (sonst ValueError). + # 2. Wenn anzahl == 0, gib eine leere Liste [] zurück. + # 3. Wenn elemente leer ist (und anzahl > 0), löse ValueError aus. + # 4. Erstelle einen zyklischen Iterator mit zyk = itertools.cycle(elemente). + # 5. Hole mit next(zyk) genau 'anzahl' Elemente ab und sammle sie in einer Liste. + # ========================================================================== + pass + + +# ============================================================================== +# DEMONSTRATION & SPEICHER-BENCHMARK +# ============================================================================== + +if __name__ == "__main__": + print("=" * 70) + print("🌊 KAPITEL 19: GENERATOREN & ITERTOOLS (MEMORY EFFICIENCY)") + print("=" * 70) + + # 1. Speichervergleich: Liste vs. Generator + elemente_anzahl = 1_000_000 + print(f"\n📊 SPEICHERVERGLEICH FÜR {elemente_anzahl:,} ZAHLEN:") + + liste = [x for x in range(elemente_anzahl)] + gen = (x for x in range(elemente_anzahl)) + + speicher_liste = sys.getsizeof(liste) / (1024 * 1024) + speicher_gen = sys.getsizeof(gen) + + print(f" 🔴 Liste (Badewanne): {speicher_liste:.2f} MB RAM im Hauptspeicher!") + print(f" 🟢 Generator (Wasserhahn): {speicher_gen} Bytes RAM (Faktor {int(speicher_liste * 1024 * 1024 / speicher_gen):,}x sparsamer!)") + + # 2. Endloszähler Test + print("\n⏱️ Endloszähler (erste 5 Werte ab 100 in 10er Schritten):") + zaehler = endlos_zaehler(100, 10) + if zaehler: + for _ in range(5): + print(f" -> {next(zaehler)}") + + # 3. Fibonacci Test + print("\n🐰 Die ersten 10 Fibonacci-Zahlen:") + fibs = fibonacci_generator(10) + if fibs: + print(f" -> {list(fibs)}") + + # 4. Passwort-Permutationen Test + print("\n🔐 Passwort-Permutationen für 'abc' (Länge 2):") + perms = erzeuge_passwort_kombinationen("abc", 2) + print(f" -> {perms}") + + # 5. Zyklischer Taktgeber + print("\n🚦 Ampel-Zyklus (7 Schritte):") + ampel = zyklische_elemente(["Rot", "Gelb", "Gruen"], 7) + print(f" -> {ampel}") + + print("\n" + "=" * 70) + print("💡 Starte die Tests mit: python3 test_aufgabe.py") + print("=" * 70) diff --git a/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/index.html b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/index.html new file mode 100644 index 0000000..1650db3 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/index.html @@ -0,0 +1,502 @@ + + + + + + Kapitel 19: Generatoren, Iteratoren & itertools + + + + +
+ + +
+
+
+ +
Lehrpfad 4 • Professional Data Engineering
+

Generatoren, Iteratoren & itertools 🌊⚡

+

Verarbeite riesige Datenströme mit 0 RAM-Verschwendung: Entdecke das Geheimnis von yield, Lazy Evaluation und Pythons High-Performance-Modul itertools.

+
+
+ +
+ + + + + +
+

1. Warum Generatoren? Die Badewannen-Analogie 🚰🛁

+ +

+ Stelle dir vor, du möchtest 10 Millionen Sensor-Messwerte analysieren. Wie verarbeitet ein Rechner diese Daten? +

+ +
+
+

🔴 Die Badewanne (Klassische Listen)

+

Du lässt erst die gesamte Badewanne vollaufen (alle 10 Mio. Werte im RAM erzeugen), bevor du einen Schluck nimmst.

+
+
# Belegt ~80 MB bis 1 GB RAM!
+daten = [x**2 for x in range(10_000_000)]
+
+ ⚠️ Gefahr: Out of Memory Crash bei Big Data! +
+ +
+

🟢 Der Wasserhahn (Generatoren mit yield)

+

Du drehst den Hahn auf: Die Daten fließen Tropfen für Tropfen (on-demand) direkt in deinen Mund.

+
+
# Belegt nur ~104 Bytes RAM!
+daten = (x**2 for x in range(10_000_000))
+
+ ✨ 100.000x speichereffizienter & ultraschnell! +
+
+ +
+
💡 Kernprinzip: Lazy Evaluation (Verzögerte Auswertung)
+

Ein Generator berechnet den nächsten Wert erst in der exakten Millisekunde, in der du ihn mit next() oder in einer for-Schleife abfragst!

+
+
+ + +
+

2. Das Protokoll: Iterable vs. Iterator 🔄

+ +

Jedes Mal, wenn du in Python eine for-Schleife schreibst, passiert Folgendes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KonzeptMagische MethodeBedeutungCode-Beispiel
Iterable__iter__()Objekt, über das iteriert werden kann.liste = [1, 2, 3]
Iterator__next__()Zeiger-Objekt, das den aktuellen Zustand hält.it = iter(liste); next(it)
StopIterationExceptionWird automatisch geworfen, wenn alle Werte verbraucht sind.Beendet for-Schleifen lautlos.
+ +
+
Iteratoren manuell steuern mit iter() und next()
+
farben = ["Rot", "Gruen", "Blau"]
+it = iter(farben)
+
+print(next(it))  # -> "Rot"
+print(next(it))  # -> "Gruen"
+print(next(it))  # -> "Blau"
+# next(it) wirft jetzt StopIteration!
+
+
+ + +
+

3. Magie im Einsatz: Eigene Generatoren mit yield 🪄

+ +

+ Im Gegensatz zu return (welches eine Funktion komplett beendet) unterbricht yield die Funktion nur kurzzeitig, gibt den Wert zurück und friert den gesamten Zustand (lokale Variablen, Zeilennummer) ein! +

+ +
+
Endloszähler & Fibonacci mit yield
+
def endlos_zaehler(start=0, schritt=1):
+    aktuell = start
+    while True:
+        yield aktuell       # Pausiert hier und gibt den Wert zurück
+        aktuell += schritt  # Macht beim nächsten next() genau hier weiter!
+
+z = endlos_zaehler(10, 5)
+print(next(z))  # -> 10
+print(next(z))  # -> 15
+print(next(z))  # -> 20
+
+
+ + +
+

4. Das itertools Power-Modul 🧰

+ +

+ Das eingebaute Modul itertools bietet blitzschnelle, in C geschriebene Iterator-Bausteine: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WerkzeugZweckBeispiel
itertools.chain(*iterables)Verkettet mehrere Listen/Generatoren ohne Kopierenchain([1, 2], [3, 4]) → 1, 2, 3, 4
itertools.cycle(iterable)Wiederholt eine Liste unendlich oft im Kreiscycle(['Rot', 'Gruen']) → Rot, Gruen, Rot...
itertools.permutations(p, r)Alle geordneten Anordnungen (Passwort-Cracker)permutations('AB', 2) → ('A','B'), ('B','A')
itertools.combinations(p, r)Alle ungeordneten Teilmengen (Lotto, Teams)combinations('ABC', 2) → ('A','B'), ('A','C'), ('B','C')
+
+ + +
+

5. 🎯 Deine Aufgaben in aufgabe.py

+ +

Öffne die Datei aufgabe.py und setze die folgenden Meilensteine um:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zu TODO 2: Fibonacci mit yield +
+

Validiere zuerst die Eingabe und iteriere mit einer Zählschleife:

+
+
if anzahl < 0:
+    raise ValueError("Anzahl darf nicht negativ sein!")
+a, b = 0, 1
+for _ in range(anzahl):
+    yield a
+    a, b = b, a + b
+
+
+
+ +
+ 🔍 Tipp zu TODO 6: Passwort-Permutationen als String-Liste +
+

itertools.permutations liefert Tupel von Zeichen. Mit "".join() machst du daraus Strings:

+
+
return ["".join(p) for p in itertools.permutations(zeichen, laenge)]
+
+
+
+
+ + +
+

6. 🧪 Lösung überprüfen & Testen

+ +

1. Automatische Tests ausführen:

+
+
+ Terminal (im Ordner 19_generatoren_und_itertools) +
+
python3 test_aufgabe.py
+
+ +

2. Benchmark-Skript starten:

+
+
+ Terminal +
+
python3 aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/musterloesung.py b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/musterloesung.py new file mode 100644 index 0000000..0eb3a0f --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/musterloesung.py @@ -0,0 +1,145 @@ +""" +Kapitel 19: Generatoren, Iteratoren & itertools (Memory Efficiency & Streaming) 🌊⚡ +================================================================================== +Musterlösung für speichereffiziente Datenverarbeitung, Generatoren und itertools. +""" + +import sys +import itertools +from typing import Iterator, Iterable, Any, Union, List, Tuple + + +# ============================================================================== +# TEIL 1: GRUNDLAGEN VON ITERATOREN & GENERATOREN MIT YIELD +# ============================================================================== + +def endlos_zaehler(start: int = 0, schritt: int = 1) -> Iterator[int]: + """ + Ein unendlicher Generator, der Zahlen beginnend bei 'start' mit der + Schrittweite 'schritt' generiert. + """ + aktuell = int(start) + schritt_wert = int(schritt) + while True: + yield aktuell + aktuell += schritt_wert + + +def fibonacci_generator(anzahl: int) -> Iterator[int]: + """ + Ein Generator, der die ersten 'anzahl' Fibonacci-Zahlen on-demand erzeugt. + """ + if anzahl < 0: + raise ValueError("Anzahl darf nicht negativ sein!") + + a, b = 0, 1 + for _ in range(anzahl): + yield a + a, b = b, a + b + + +# ============================================================================== +# TEIL 2: SPEICHERSCHONENDE FILTER & GENERATOR-EXPRESSIONS +# ============================================================================== + +def filter_grosse_zahlen(zahlen_iterable: Iterable[Union[int, float]], schwellenwert: float) -> Iterator[Union[int, float]]: + """ + Ein Generator-Filter, der nur Zahlen liefert, die strikt größer als + der angegebene 'schwellenwert' sind (x > schwellenwert). + """ + for x in zahlen_iterable: + if x > schwellenwert: + yield x + + +def erzeuge_quadrat_generator(n: int) -> Iterator[int]: + """ + Erzeugt eine Generator-Expression für Quadratzahlen von 1 bis n (inklusive). + """ + if n < 0: + raise ValueError("n darf nicht negativ sein!") + return (x ** 2 for x in range(1, n + 1)) + + +# ============================================================================== +# TEIL 3: DAS ITERTOOLS-MODUL (CHAIN, PERMUTATIONS & CYCLE) +# ============================================================================== + +def kette_listen(*listen: Iterable[Any]) -> List[Any]: + """ + Verbindet beliebig viele Iterables (*args) mit 'itertools.chain' zu einer + einzigen flachen Liste. + """ + return list(itertools.chain(*listen)) + + +def erzeuge_passwort_kombinationen(zeichen: str, laenge: int) -> List[str]: + """ + Erzeugt alle möglichen Permutationen der angegebenen Zeichenkette mit der + exakten Länge 'laenge' unter Verwendung von 'itertools.permutations'. + """ + if laenge < 0 or laenge > len(zeichen): + raise ValueError(f"Ungültige Länge {laenge} für Zeichenkette der Länge {len(zeichen)}!") + + return ["".join(p) for p in itertools.permutations(zeichen, laenge)] + + +def zyklische_elemente(elemente: List[Any], anzahl: int) -> List[Any]: + """ + Nimmt eine Liste von Elementen und liefert die ersten 'anzahl' Elemente + in zyklischer Reihenfolge mit 'itertools.cycle'. + """ + if anzahl < 0: + raise ValueError("Anzahl darf nicht negativ sein!") + if anzahl == 0: + return [] + if not elemente: + raise ValueError("Elemente-Liste darf nicht leer sein bei anzahl > 0!") + + zyk = itertools.cycle(elemente) + return [next(zyk) for _ in range(anzahl)] + + +# ============================================================================== +# DEMONSTRATION & SPEICHER-BENCHMARK +# ============================================================================== + +if __name__ == "__main__": + print("=" * 70) + print("🌊 KAPITEL 19: MUSTERLÖSUNG – GENERATOREN & ITERTOOLS") + print("=" * 70) + + # 1. Speichervergleich: Liste vs. Generator + elemente_anzahl = 1_000_000 + print(f"\n📊 SPEICHERVERGLEICH FÜR {elemente_anzahl:,} ZAHLEN:") + + liste = [x for x in range(elemente_anzahl)] + gen = (x for x in range(elemente_anzahl)) + + speicher_liste = sys.getsizeof(liste) / (1024 * 1024) + speicher_gen = sys.getsizeof(gen) + + print(f" 🔴 Liste (Badewanne): {speicher_liste:.2f} MB RAM im Hauptspeicher!") + print(f" 🟢 Generator (Wasserhahn): {speicher_gen} Bytes RAM (Faktor {int(speicher_liste * 1024 * 1024 / speicher_gen):,}x sparsamer!)") + + # 2. Endloszähler Test + print("\n⏱️ Endloszähler (erste 5 Werte ab 100 in 10er Schritten):") + zaehler = endlos_zaehler(100, 10) + for _ in range(5): + print(f" -> {next(zaehler)}") + + # 3. Fibonacci Test + print("\n🐰 Die ersten 10 Fibonacci-Zahlen:") + print(f" -> {list(fibonacci_generator(10))}") + + # 4. Passwort-Permutationen Test + print("\n🔐 Passwort-Permutationen für 'abc' (Länge 2):") + print(f" -> {erzeuge_passwort_kombinationen('abc', 2)}") + + # 5. Zyklischer Taktgeber + print("\n🚦 Ampel-Zyklus (7 Schritte):") + print(f" -> {zyklische_elemente(['Rot', 'Gelb', 'Gruen'], 7)}") + + print("\n" + "=" * 70) + print("✅ Musterlösung erfolgreich ausgeführt!") + print("=" * 70) diff --git a/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/test_aufgabe.py new file mode 100644 index 0000000..0759a51 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools/test_aufgabe.py @@ -0,0 +1,190 @@ +import unittest +import sys +import types +import inspect +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel19(unittest.TestCase): + """ + Testsuite für Kapitel 19: Generatoren, Iteratoren & itertools (Memory Efficiency). + """ + + def setUp(self): + self.endlos_zaehler = getattr(aufgabe, "endlos_zaehler", None) + self.fibonacci_generator = getattr(aufgabe, "fibonacci_generator", None) + self.filter_grosse_zahlen = getattr(aufgabe, "filter_grosse_zahlen", None) + self.erzeuge_quadrat_generator = getattr(aufgabe, "erzeuge_quadrat_generator", None) + self.kette_listen = getattr(aufgabe, "kette_listen", None) + self.erzeuge_passwort_kombinationen = getattr(aufgabe, "erzeuge_passwort_kombinationen", None) + self.zyklische_elemente = getattr(aufgabe, "zyklische_elemente", None) + + self.assertIsNotNone(self.endlos_zaehler, "Funktion 'endlos_zaehler' nicht gefunden!") + self.assertIsNotNone(self.fibonacci_generator, "Funktion 'fibonacci_generator' nicht gefunden!") + self.assertIsNotNone(self.filter_grosse_zahlen, "Funktion 'filter_grosse_zahlen' nicht gefunden!") + self.assertIsNotNone(self.erzeuge_quadrat_generator, "Funktion 'erzeuge_quadrat_generator' nicht gefunden!") + self.assertIsNotNone(self.kette_listen, "Funktion 'kette_listen' nicht gefunden!") + self.assertIsNotNone(self.erzeuge_passwort_kombinationen, "Funktion 'erzeuge_passwort_kombinationen' nicht gefunden!") + self.assertIsNotNone(self.zyklische_elemente, "Funktion 'zyklische_elemente' nicht gefunden!") + + # ========================================================================== + # 1. Endloszähler (Generator mit yield) + # ========================================================================== + def test_01_endlos_zaehler(self): + """Prüft, ob endlos_zaehler ein echter Generator ist und korrekt zählt.""" + gen = self.endlos_zaehler(start=0, schritt=1) + self.assertTrue(inspect.isgenerator(gen) or isinstance(gen, types.GeneratorType), + "endlos_zaehler muss ein Generator sein (yield nutzen)!") + + # Erste 5 Werte prüfen + werte = [next(gen) for _ in range(5)] + self.assertEqual(werte, [0, 1, 2, 3, 4]) + + # Mit benutzerdefiniertem Start und Schrittweite + gen2 = self.endlos_zaehler(start=100, schritt=5) + werte2 = [next(gen2) for _ in range(4)] + self.assertEqual(werte2, [100, 105, 110, 115]) + + # Mit negativer Schrittweite + gen3 = self.endlos_zaehler(start=10, schritt=-2) + werte3 = [next(gen3) for _ in range(4)] + self.assertEqual(werte3, [10, 8, 6, 4]) + + # ========================================================================== + # 2. Fibonacci-Generator + # ========================================================================== + def test_02_fibonacci_generator(self): + """Prüft Fibonacci-Generator für verschiedene Anzahlen und Fehlerbehandlung.""" + gen = self.fibonacci_generator(7) + self.assertTrue(inspect.isgenerator(gen) or isinstance(gen, types.GeneratorType), + "fibonacci_generator muss ein Generator sein!") + + ergebnis = list(gen) + self.assertEqual(ergebnis, [0, 1, 1, 2, 3, 5, 8]) + + # Grenzfälle + self.assertEqual(list(self.fibonacci_generator(0)), []) + self.assertEqual(list(self.fibonacci_generator(1)), [0]) + self.assertEqual(list(self.fibonacci_generator(2)), [0, 1]) + self.assertEqual(list(self.fibonacci_generator(10)), [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]) + + # Negative Anzahl muss ValueError werfen + with self.assertRaises(ValueError, msg="Negative Anzahl muss ValueError auslösen!"): + list(self.fibonacci_generator(-5)) + + # ========================================================================== + # 3. Speicherschonender Filter (filter_grosse_zahlen) + # ========================================================================== + def test_03_filter_grosse_zahlen(self): + """Prüft, ob der Filter Zahlen speicherschonend per Generator filtert.""" + eingabe = [10, 2.5, 50, 100, 5, 20.1] + gen = self.filter_grosse_zahlen(eingabe, 20.0) + self.assertTrue(inspect.isgenerator(gen) or isinstance(gen, types.GeneratorType), + "filter_grosse_zahlen muss ein Generator sein (yield nutzen)!") + + ergebnis = list(gen) + self.assertEqual(ergebnis, [50, 100, 20.1]) + + # Keine Treffer + self.assertEqual(list(self.filter_grosse_zahlen([1, 2, 3], 10)), []) + + # Alle Treffer + self.assertEqual(list(self.filter_grosse_zahlen([15, 20, 30], 10)), [15, 20, 30]) + + # Funktioniert auch mit einem Generator als Eingabe (Streaming-Pipeline) + eingabe_gen = (x * 10 for x in range(5)) # 0, 10, 20, 30, 40 + gefiltert = list(self.filter_grosse_zahlen(eingabe_gen, 15)) + self.assertEqual(gefiltert, [20, 30, 40]) + + # ========================================================================== + # 4. Generator Expression (erzeuge_quadrat_generator) + # ========================================================================== + def test_04_erzeuge_quadrat_generator(self): + """Prüft die Erzeugung von Generator Expressions.""" + gen = self.erzeuge_quadrat_generator(5) + self.assertTrue(inspect.isgenerator(gen) or isinstance(gen, types.GeneratorType), + "erzeuge_quadrat_generator muss eine Generator-Expression zurückgeben!") + + self.assertEqual(list(gen), [1, 4, 9, 16, 25]) + self.assertEqual(list(self.erzeuge_quadrat_generator(0)), []) + + with self.assertRaises(ValueError, msg="Negatives n muss ValueError auslösen!"): + self.erzeuge_quadrat_generator(-3) + + # ========================================================================== + # 5. itertools.chain (kette_listen) + # ========================================================================== + def test_05_kette_listen(self): + """Prüft das Verketten beliebiger Iterables mit itertools.chain.""" + l1 = [1, 2] + l2 = [3, 4, 5] + l3 = [6] + self.assertEqual(self.kette_listen(l1, l2, l3), [1, 2, 3, 4, 5, 6]) + + # Leere Iterables + self.assertEqual(self.kette_listen([], [42], []), [42]) + self.assertEqual(self.kette_listen(), []) + + # Gemischte Iterables (Tupel, Sets, Generatoren) + t = (10, 20) + g = (x for x in [30, 40]) + self.assertEqual(self.kette_listen(t, g), [10, 20, 30, 40]) + + # ========================================================================== + # 6. itertools.permutations (erzeuge_passwort_kombinationen) + # ========================================================================== + def test_06_erzeuge_passwort_kombinationen(self): + """Prüft die Erzeugung von Permutationen mit itertools.permutations.""" + kombis = self.erzeuge_passwort_kombinationen("abc", 2) + erwartet = ['ab', 'ac', 'ba', 'bc', 'ca', 'cb'] + self.assertEqual(sorted(kombis), sorted(erwartet)) + self.assertEqual(len(kombis), 6) + + # Einzelzeichen + kombis1 = self.erzeuge_passwort_kombinationen("xyz", 1) + self.assertEqual(sorted(kombis1), ['x', 'y', 'z']) + + # Gesamtlänge + kombis_voll = self.erzeuge_passwort_kombinationen("ab", 2) + self.assertEqual(sorted(kombis_voll), ['ab', 'ba']) + + # Länge 0 + self.assertEqual(self.erzeuge_passwort_kombinationen("abc", 0), ['']) + + # Fehlerbehandlung bei ungültigen Längen + with self.assertRaises(ValueError, msg="Negative Länge muss ValueError werfen"): + self.erzeuge_passwort_kombinationen("abc", -1) + + with self.assertRaises(ValueError, msg="Länge > Zeichenanzahl muss ValueError werfen"): + self.erzeuge_passwort_kombinationen("ab", 5) + + # ========================================================================== + # 7. itertools.cycle (zyklische_elemente) + # ========================================================================== + def test_07_zyklische_elemente(self): + """Prüft zyklische Entnahme mit itertools.cycle.""" + farben = ["Rot", "Gelb", "Gruen"] + res = self.zyklische_elemente(farben, 7) + self.assertEqual(res, ["Rot", "Gelb", "Gruen", "Rot", "Gelb", "Gruen", "Rot"]) + + # Anzahl 0 + self.assertEqual(self.zyklische_elemente(farben, 0), []) + + # Fehlerbehandlung + with self.assertRaises(ValueError, msg="Negative Anzahl muss ValueError werfen"): + self.zyklische_elemente(farben, -2) + + with self.assertRaises(ValueError, msg="Leere Liste bei anzahl > 0 muss ValueError werfen"): + self.zyklische_elemente([], 5) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/README.md b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/README.md new file mode 100644 index 0000000..68b2a1a --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/README.md @@ -0,0 +1,141 @@ +# Kapitel 20: Dataclasses, Type Hints & Decorators (Moderne Python-Architektur) 🏷️✨ + +Willkommen zu **Kapitel 20** im Lehrpfad 4 (*Professional Data Engineering*)! +In diesem Modul lernst du die Kernbausteine moderner, robuster Python-Architekturen kennen: +1. **Dataclasses (`@dataclass`)**: Schluss mit endlosem Boilerplate-Code für `__init__`, `__repr__` und `__eq__`! +2. **Fortgeschrittene Type Hints**: Statische Typsicherheit mit `Optional`, `Union`, `Callable` und `Dict`. +3. **Eigene Decorators**: Funktionserweiterungen wie Logging, Zeitmessung und Zugriffskontrollen als wiederverwendbare Hüllen. +4. **Standard-Decorators**: `@property`, `@classmethod` und `@staticmethod`. + +--- + +## 🧭 Didaktische Analogien + +### A) Das Fertighaus / Die Gussform (`@dataclass`) +Statt jeden Ziegelstein für den Konstruktor (`def __init__(self, a, b, c): self.a = a...`), den String-Druck (`def __repr__`) und Vergleiche (`def __eq__`) von Hand zu mauern, gibst du Python einfach die **Gussform** (die Feld-Typen). Python gießt das gesamte Klassenfundament automatisch in Millisekunden! + +### B) Die Geschenkverpackung / Schutzhülle (Decorators) +Ein Decorator nimmt eine existierende Funktion, wickelt eine schützende Hülle (den Wrapper) darum und gibt das eingepackte Geschenk zurück. Die ursprüngliche Funktion bleibt unverändert, erhält aber Superkräfte (z.B. Zeitmessung oder Aufrufzählung)! + +``` + ┌────────────────────────────────────────────────────────┐ + │ 🎁 DECORATOR WRAPPER │ + │ │ + │ [1. Vorher] ⏱️ Startzeit erfassen / Zähler +1 │ + │ [2. Kern] ▶️ Originalfunktion ausführen │ + │ [3. Nachher] 📊 Dauer berechnen & loggen │ + │ │ + └────────────────────────────────────────────────────────┘ +``` + +--- + +## 📦 1. Dataclasses: Schluss mit Boilerplate-Code + +### Vor Python 3.7 (Klassischer Boilerplate): +```python +class ArtikelKlassisch: + def __init__(self, name: str, preis: float, kategorie: str = "Allgemein"): + self.name = name + self.preis = preis + self.kategorie = kategorie + + def __repr__(self): + return f"ArtikelKlassisch(name='{self.name}', preis={self.preis}, kategorie='{self.kategorie}')" + + def __eq__(self, other): + if not isinstance(other, ArtikelKlassisch): + return False + return (self.name, self.preis, self.kategorie) == (other.name, other.preis, other.kategorie) +``` + +### Modern mit `@dataclass`: +```python +from dataclasses import dataclass, field + +@dataclass +class Artikel: + name: str + preis: float + kategorie: str = "Allgemein" +``` +Python erzeugt `__init__`, `__repr__` und `__eq__` vollautomatisch! + +### ⚠️ Wichtig: Veränderliche Standardwerte mit `field(default_factory=...)` +Listen oder Dictionaries dürfen in Dataclasses niemals als `artikel: list = []` definiert werden (da alle Instanzen sonst dieselbe Liste teilen würden). Nutze stattdessen immer `field(default_factory=list)`: + +```python +@dataclass +class Warenkorb: + kunde: str + artikel_liste: list[Artikel] = field(default_factory=list) +``` + +--- + +## 🏷️ 2. Fortgeschrittene Type Hints + +Pythons `typing`-Modul macht deinen Code selbsterklärend und schützt vor bösen Überraschungen: + +| Type Hint | Bedeutung | Beispiel | +| :--- | :--- | :--- | +| `Optional[T]` | Wert vom Typ `T` oder `None` (`Union[T, None]`) | `Optional[str]` (z.B. `"Max"` oder `None`) | +| `Union[A, B]` | Wert kann entweder Typ `A` oder Typ `B` sein | `Union[int, float]` | +| `Callable[[Arg1, Arg2], ReturnType]` | Eine Funktion als Argument oder Rückgabe | `Callable[[int, int], bool]` | +| `List[T]`, `Dict[K, V]` | Typisierte Listen und Key-Value-Mappings | `Dict[str, Union[str, int]]` | + +--- + +## 🎨 3. Eigene Decorators schreiben + +Ein Decorator ist eine Funktion höherer Ordnung, die eine Funktion entgegennimmt und eine neue Wrapper-Funktion zurückliefert. Nutze immer `@functools.wraps(func)`, um den ursprünglichen Namen und Docstring zu erhalten: + +```python +import functools +import time + +def zeitmessung(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + ergebnis = func(*args, **kwargs) + dauer = time.perf_counter() - start + print(f"[⏱️] {func.__name__} dauerte {dauer:.4f}s") + return ergebnis + return wrapper + +@zeitmessung +def berechne_daten(): + time.sleep(0.1) + return 42 +``` + +--- + +## ⚙️ 4. Standard-Decorators im Überblick + +- **`@property`**: Ermöglicht den Aufruf einer Methode wie ein Attribut (`auto.geschwindigkeit` statt `auto.get_geschwindigkeit()`). +- **`@classmethod`**: Erhält die Klasse `cls` als ersten Parameter; ideal für alternative Konstruktoren (Factory Methods wie `Artikel.aus_csv_zeile(zeile)`). +- **`@staticmethod`**: Reine Hilfsfunktion innerhalb des Klassen-Namespaces ohne Zugriff auf `self` oder `cls`. + +--- + +## 📁 Die Dateien in diesem Ordner + +- **`index.html`**: Interaktive Web-Lernseite mit visuellen Ablaufdiagrammen, Vorher-Nachher-Vergleichen, Cheat-Sheets und Checkliste. +- **`aufgabe.py`**: Dein Arbeitsblatt mit Type Hints und didaktischen TODOs für Dataclasses, Warenkorb, Decorators und Typprüfung. +- **`test_aufgabe.py`**: Automatische Unittest-Suite zur Überprüfung aller Anforderungen. +- **`musterloesung.py`**: Vollständig ausprogrammierte Referenzlösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Führe die automatischen Tests aus: +python3 test_aufgabe.py + +# 3. Oder starte das interaktive Skript: +python3 aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/aufgabe.py b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/aufgabe.py new file mode 100644 index 0000000..6cbb8be --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/aufgabe.py @@ -0,0 +1,286 @@ +""" +Kapitel 20: Dataclasses, Type Hints & Decorators (Moderne Python-Architektur) 🏷️✨ +================================================================================== +Lerne moderne Python-Architekturmuster für typsicheren, sauberen Code: +1. @dataclass (Automatische Konstruktoren, __repr__, __eq__, field(default_factory=...)) +2. Fortgeschrittene Type Hints (Optional, Union, Callable, Dict, List) +3. Eigene Decorators schreiben (@functools.wraps, Wrapper-Funktionen) +4. Standard-Decorators (@property, @classmethod, @staticmethod) + +Didaktische Analogie: +- "Gussform / Fertighaus" (@dataclass): Spart seitenlangen Boilerplate-Code für __init__, __repr__ etc. +- "Geschenkverpackung / Schutzhülle" (Decorator): Erweitert Funktionen um Logging, Zähler oder Zeitmessung, ohne ihren Code zu ändern. +""" + +import time +import functools +from dataclasses import dataclass, field +from typing import Optional, Union, Callable, List, Dict, Any + + +# ============================================================================== +# TEIL 1: DATACLASSES FÜR TYPSICHERE DATENMODELLE +# ============================================================================== + +@dataclass +class Artikel: + """ + Ein Artikel im Online-Shop als moderne Dataclass. + + Attribute: + name (str): Artikelbezeichnung (darf nicht leer sein). + preis (float): Nettopreis in Euro (muss >= 0 sein). + kategorie (str): Warengruppe (Standard: 'Allgemein'). + """ + name: str + preis: float + kategorie: str = "Allgemein" + + def __post_init__(self) -> None: + """ + Wird automatisch direkt nach dem generierten __init__ aufgerufen. + Validiere hier die Daten! + """ + # ====================================================================== + # 🎯 TEILZIEL 1 (TODO 1): Validierung in __post_init__ + # 1. Wenn name leer oder nur Leerzeichen ist, löse ValueError aus. + # 2. Wenn preis < 0 ist, löse ValueError aus. + # ====================================================================== + pass + + def berechne_bruttopreis(self, mwst_satz: float = 0.19) -> float: + """ + Berechnet den Bruttopreis inklusive Mehrwertsteuer, gerundet auf 2 Nachkommastellen. + + Parameter: + mwst_satz (float): Mehrwertsteuersatz als Dezimalzahl (z.B. 0.19 für 19%). + + Exceptions: + ValueError: Falls mwst_satz < 0 ist. + + Beispiele: + Artikel("Buch", 10.0).berechne_bruttopreis(0.07) -> 10.7 + Artikel("Maus", 20.0).berechne_bruttopreis(0.19) -> 23.8 + """ + # ====================================================================== + # 🎯 TEILZIEL 2 (TODO 2): Implementiere berechne_bruttopreis + # 1. Validiere: mwst_satz darf nicht negativ (< 0) sein. + # 2. Berechne: preis * (1.0 + mwst_satz) + # 3. Runde auf 2 Dezimalstellen mit round(..., 2) und gib den Wert zurück. + # ====================================================================== + pass + + @classmethod + def aus_csv_zeile(cls, csv_zeile: str) -> "Artikel": + """ + Erstellt ein Artikel-Objekt aus einer CSV-Zeile wie 'Laptop;999.99;Elektronik'. + """ + # ====================================================================== + # 🎯 TEILZIEL 3 (TODO 3): Implementiere die @classmethod aus_csv_zeile + # 1. Trenne die csv_zeile an Semikolons (csv_zeile.split(';')). + # 2. Entpacke name, preis (als float) und optional kategorie. + # 3. Erzeuge und gib eine neue Instanz mit cls(...) zurück. + # ====================================================================== + pass + + @staticmethod + def ist_gueltiger_preis(preis: float) -> bool: + """ + Prüft statisch, ob ein Preis gültig (numerisch und >= 0) ist. + """ + # ====================================================================== + # 🎯 TEILZIEL 4 (TODO 4): Implementiere die @staticmethod ist_gueltiger_preis + # 1. Gib True zurück, wenn preis ein int oder float ist und preis >= 0.0. + # ====================================================================== + pass + + +@dataclass +class Warenkorb: + """ + Ein digitaler Warenkorb, der Artikel sammelt und verwaltet. + + Attribute: + kunde (str): Name des Kunden. + artikel_liste (list[Artikel]): Liste der Artikel (nutze field(default_factory=list)!). + """ + kunde: str + artikel_liste: List[Artikel] = field(default_factory=list) + + def artikel_hinzufuegen(self, *artikel: Artikel) -> None: + """ + Fügt beliebig viele Artikel (*args) zum Warenkorb hinzu. + + Exceptions: + TypeError: Falls ein Element kein Artikel-Objekt ist. + """ + # ====================================================================== + # 🎯 TEILZIEL 5 (TODO 5): Implementiere artikel_hinzufuegen + # 1. Iteriere über alle übergebenen Artikel. + # 2. Prüfe mit isinstance(a, Artikel), ob der Typ stimmt (sonst TypeError). + # 3. Hänge das Element an self.artikel_liste an. + # ====================================================================== + pass + + def artikel_entfernen(self, artikel_name: str) -> bool: + """ + Entfernt den ersten Artikel mit dem passenden Namen (Groß-/Kleinschreibung ignorieren). + Gibt True zurück, wenn der Artikel gefunden und entfernt wurde, sonst False. + """ + # ====================================================================== + # 🎯 TEILZIEL 6 (TODO 6): Implementiere artikel_entfernen + # 1. Durchsuche self.artikel_liste nach einem Artikel mit a.name.lower() == artikel_name.lower(). + # 2. Entferne ihn mit self.artikel_liste.remove(a) und gib True zurück. + # 3. Wenn nicht gefunden, gib False zurück. + # ====================================================================== + pass + + def gesamtsumme(self, brutto: bool = False, mwst_satz: float = 0.19) -> float: + """ + Berechnet die Gesamtsumme aller Artikel im Warenkorb. + + Parameter: + brutto (bool): Wenn True, wird der Bruttopreis mit mwst_satz summiert. + mwst_satz (float): Der MwSt-Satz bei Bruttoberechnung. + + Rückgabe: + float: Die auf 2 Stellen gerundete Gesamtsumme. + """ + # ====================================================================== + # 🎯 TEILZIEL 7 (TODO 7): Implementiere gesamtsumme + # 1. Wenn brutto == True: Summiere a.berechne_bruttopreis(mwst_satz) für alle Artikel. + # 2. Wenn brutto == False: Summiere a.preis für alle Artikel. + # 3. Runde auf 2 Nachkommastellen und gib den Wert zurück (bei leerem Korb 0.0). + # ====================================================================== + pass + + def __len__(self) -> int: + """Gibt die Anzahl der Artikel im Warenkorb zurück.""" + return len(self.artikel_liste) + + +# ============================================================================== +# TEIL 2: EIGENE DECORATORS SCHREIBEN +# ============================================================================== + +def aufruf_zaehler(func: Callable) -> Callable: + """ + Ein Decorator, der zählt, wie oft eine dekorierte Funktion aufgerufen wurde. + Der Zähler wird als Attribut 'aufrufe' an der Wrapper-Funktion hinterlegt. + + Beispiel: + @aufruf_zaehler + def gruss(name): + return f"Hallo {name}" + + gruss("Anna") + gruss.aufrufe # -> 1 + gruss("Ben") + gruss.aufrufe # -> 2 + """ + # ========================================================================== + # 🎯 TEILZIEL 8 (TODO 8): Implementiere den Decorator aufruf_zaehler + # 1. Definiere die innere Funktion wrapper(*args, **kwargs). + # 2. Nutze @functools.wraps(func) über der wrapper-Funktion. + # 3. Erhöhe wrapper.aufrufe um 1. + # 4. Führe func(*args, **kwargs) aus und gib das Ergebnis zurück. + # 5. Initialisiere wrapper.aufrufe = 0 vor der Rückgabe von wrapper. + # ========================================================================== + pass + + +def zeitmessung(func: Callable) -> Callable: + """ + Ein Decorator, der die Ausführungszeit einer Funktion misst und die Dauer + als Attribut 'letzte_dauer' (in Sekunden) an der Wrapper-Funktion speichert. + """ + # ========================================================================== + # 🎯 TEILZIEL 9 (TODO 9): Implementiere den Decorator zeitmessung + # 1. Definiere wrapper(*args, **kwargs) mit @functools.wraps(func). + # 2. Miss die Startzeit mit time.perf_counter(). + # 3. Führe das Original func(*args, **kwargs) aus. + # 4. Berechne dauer = time.perf_counter() - start_zeit. + # 5. Speichere wrapper.letzte_dauer = dauer. + # 6. Gib das Funktionsergebnis zurück. + # 7. Initialisiere wrapper.letzte_dauer = 0.0 vor der Rückgabe von wrapper. + # ========================================================================== + pass + + +# ============================================================================== +# TEIL 3: FORTGESCHRITTENE TYPE HINTS (UNION, DICT, TYPED DATA) +# ============================================================================== + +def formatiere_benutzer(daten: Dict[str, Union[str, int, bool]]) -> str: + """ + Formatiert ein Benutzer-Dictionary mit verschiedenen Datentypen zu einem + lesbaren Status-String. + + Erwartete Schlüssel im Dict: + - "username" (str, Pflicht): Der Benutzername. + - "alter" (int, optional): Alter in Jahren. + - "ist_admin" (bool, optional): Ob der Benutzer Administratorrechte besitzt. + + Rückgabe: + str: Formatierter String, z.B. + "Benutzer: Alice | Alter: 28 | Rolle: Administrator" oder + "Benutzer: Bob | Alter: Unbekannt | Rolle: Standardbenutzer" + + Exceptions: + KeyError: Falls der Pflichtschlüssel "username" fehlt. + ValueError: Falls "username" ein leerer String ist. + """ + # ========================================================================== + # 🎯 TEILZIEL 10 (TODO 10): Implementiere formatiere_benutzer + # 1. Prüfe, ob "username" in daten enthalten ist (wenn nicht -> KeyError). + # 2. Prüfe, ob daten["username"] ein nicht-leerer String ist (wenn nicht -> ValueError). + # 3. Lese das Alter: Wenn vorhanden, nutze den Wert, sonst "Unbekannt". + # 4. Lese "ist_admin": Wenn True, rolle = "Administrator", sonst "Standardbenutzer". + # 5. Gib den String im Format: + # "Benutzer: {username} | Alter: {alter} | Rolle: {rolle}" zurück. + # ========================================================================== + pass + + +# ============================================================================== +# DEMONSTRATION & MAIN +# ============================================================================== + +if __name__ == "__main__": + print("=" * 70) + print("🏷️ KAPITEL 20: DATACLASSES, TYPE HINTS & DECORATORS") + print("=" * 70) + + # 1. Dataclass Artikel testen + laptop = Artikel(name="ThinkPad X1", preis=1499.00, kategorie="Elektronik") + print(f"\n📦 Artikel Dataclass:\n {laptop}") + print(f" Bruttopreis (19%): {laptop.berechne_bruttopreis(0.19):.2f} €") + + # 2. Warenkorb testen + korb = Warenkorb(kunde="Max Mustermann") + korb.artikel_hinzufuegen( + laptop, + Artikel("USB-C Hub", 39.90, "Zubehoer"), + Artikel("Kaffeebecher", 12.50, "Haushalt") + ) + print(f"\n🛒 Warenkorb für {korb.kunde} ({len(korb)} Artikel):") + print(f" Nettosumme: {korb.gesamtsumme():.2f} €") + print(f" Bruttosumme: {korb.gesamtsumme(brutto=True):.2f} €") + + # 3. Aufruf-Zähler Decorator + @aufruf_zaehler + def sage_hallo(name: str) -> str: + return f"Hallo, {name}!" + + print("\n🔢 Aufruf-Zähler Decorator Test:") + print(sage_hallo("Alice")) + print(sage_hallo("Bob")) + print(f" Anzahl Aufrufe: {sage_hallo.aufrufe}") + + # 4. Typsicherer Benutzer-Formatierer + user = {"username": "CoderGirl99", "alter": 24, "ist_admin": True} + print(f"\n👤 Benutzerdaten:\n {formatiere_benutzer(user)}") + + print("\n" + "=" * 70) + print("💡 Starte die Tests mit: python3 test_aufgabe.py") + print("=" * 70) diff --git a/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/index.html b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/index.html new file mode 100644 index 0000000..d48eda8 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/index.html @@ -0,0 +1,478 @@ + + + + + + Kapitel 20: Dataclasses, Type Hints & Decorators + + + + +
+ + +
+
+
+ +
Lehrpfad 4 • Professional Data Engineering
+

Dataclasses, Type Hints & Decorators 🏷️✨

+

Schreibe eleganten, selbstdokumentierenden und typsicheren Code mit @dataclass, modernen Type Hints und maßgeschneiderten Decorators.

+
+
+ +
+ + + + + +
+

1. @dataclass: Die Fertighaus-Gussform 🏗️📦

+ +

+ In vielen Programmen dienen Klassen primär als Datenbehälter. Bisher musstest du dafür seitenweise denselben Standard-Code schreiben: +

+ +
+
+

🔴 Klassisch (Viel Tipparbeit)

+
+
class Artikel:
+    def __init__(self, name, preis):
+        self.name = name
+        self.preis = preis
+
+    def __repr__(self):
+        return f"Artikel({self.name}, {self.preis})"
+
+    def __eq__(self, other):
+        return (self.name, self.preis) == (other.name, other.preis)
+
+
+ +
+

🟢 Modern mit @dataclass

+
+
from dataclasses import dataclass, field
+
+@dataclass
+class Artikel:
+    name: str
+    preis: float
+    kategorie: str = "Allgemein"
+
+ ✨ __init__, __repr__ und __eq__ sind automatisch fertig! +
+
+ +
+
⚠️ Wichtig: Veränderliche Default-Listen mit field(default_factory=list)
+

Schreibe in einer Dataclass niemals liste: list = [], da sonst alle Instanzen dieselbe Liste teilen! Nutze stattdessen immer field(default_factory=list).

+
+
+ + +
+

2. Moderne Type Hints im Überblick 🏷️

+ +

+ Type Hints machen deinen Code selbsterklärend und ermöglichen IDEs wie VS Code blitzschnelle Autovervollständigung und Fehlerwarnungen: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type HintBedeutungPraxis-Beispiel
Optional[str]Kann str oder None seindef suche(id: int) -> Optional[User]: ...
Union[int, float]Zahl kann Ganzzahl oder Fließkomma seindef runden(wert: Union[int, float]) -> float: ...
Callable[[A, B], C]Eine übergebene Funktiondef ausfuehren(func: Callable) -> None: ...
Dict[str, Any]Schlüssel sind Strings, Werte beliebiguser_config: Dict[str, Any] = {"theme": "dark"}
+
+ + +
+

3. Eigene Decorators schreiben: Das Geschenkverpackungs-Muster 🎁

+ +

+ Ein Decorator ist eine Funktion, die eine andere Funktion einpackt und ihr zusätzliche Fähigkeiten verleiht (z.B. Zeitmessung, Logging, Aufrufzählung): +

+ +
+
Eigener Zeitmessungs-Decorator mit @functools.wraps
+
import time
+import functools
+
+def zeitmessung(func):
+    @functools.wraps(func)  # Erhält Name und Docstring der Originalfunktion
+    def wrapper(*args, **kwargs):
+        start = time.perf_counter()
+        ergebnis = func(*args, **kwargs)
+        dauer = time.perf_counter() - start
+        print(f"[⏱️] {func.__name__} brauchte {dauer:.6f}s")
+        return ergebnis
+    return wrapper
+
+@zeitmessung
+def schwere_berechnung():
+    return sum(x**2 for x in range(500_000))
+
+
+ + +
+

4. 🎯 Deine Aufgaben in aufgabe.py

+ +

Öffne die Datei aufgabe.py und hake die Schritte nach und nach ab:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zu TODO 8: Aufruf-Zähler als Decorator +
+

Initialisiere das Attribut am Wrapper vor der Rückgabe:

+
+
def aufruf_zaehler(func):
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        wrapper.aufrufe += 1
+        return func(*args, **kwargs)
+    wrapper.aufrufe = 0
+    return wrapper
+
+
+
+ +
+ 🔍 Tipp zu TODO 3: @classmethod als Fabrikmethode +
+

cls ist die Klasse selbst. Teile den String und rufe cls(...) auf:

+
+
@classmethod
+def aus_csv_zeile(cls, csv_zeile: str) -> "Artikel":
+    teile = [t.strip() for t in csv_zeile.split(";")]
+    return cls(name=teile[0], preis=float(teile[1]), kategorie=teile[2] if len(teile) > 2 else "Allgemein")
+
+
+
+
+ + +
+

5. 🧪 Lösung überprüfen & Testen

+ +

1. Automatische Tests ausführen:

+
+
+ Terminal (im Ordner 20_dataclasses_und_typehints) +
+
python3 test_aufgabe.py
+
+ +

2. Interaktives Skript starten:

+
+
+ Terminal +
+
python3 aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/musterloesung.py b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/musterloesung.py new file mode 100644 index 0000000..4eea2ff --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/musterloesung.py @@ -0,0 +1,212 @@ +""" +Kapitel 20: Dataclasses, Type Hints & Decorators (Moderne Python-Architektur) 🏷️✨ +================================================================================== +Musterlösung für moderne Python-Architektur mit Dataclasses, Typisierung und Decorators. +""" + +import time +import functools +from dataclasses import dataclass, field +from typing import Optional, Union, Callable, List, Dict, Any + + +# ============================================================================== +# TEIL 1: DATACLASSES FÜR TYPSICHERE DATENMODELLE +# ============================================================================== + +@dataclass +class Artikel: + """ + Ein Artikel im Online-Shop als moderne Dataclass. + """ + name: str + preis: float + kategorie: str = "Allgemein" + + def __post_init__(self) -> None: + if not self.name or not str(self.name).strip(): + raise ValueError("Artikelname darf nicht leer sein!") + if float(self.preis) < 0.0: + raise ValueError("Preis darf nicht negativ sein!") + + def berechne_bruttopreis(self, mwst_satz: float = 0.19) -> float: + """ + Berechnet den Bruttopreis inklusive Mehrwertsteuer, gerundet auf 2 Nachkommastellen. + """ + if float(mwst_satz) < 0.0: + raise ValueError("Mehrwertsteuersatz darf nicht negativ sein!") + return round(float(self.preis) * (1.0 + float(mwst_satz)), 2) + + @classmethod + def aus_csv_zeile(cls, csv_zeile: str) -> "Artikel": + """ + Erstellt ein Artikel-Objekt aus einer CSV-Zeile wie 'Laptop;999.99;Elektronik'. + """ + teile = [t.strip() for t in csv_zeile.split(";")] + if len(teile) < 2: + raise ValueError("CSV-Zeile muss mindestens Name und Preis enthalten!") + + name = teile[0] + preis = float(teile[1]) + kategorie = teile[2] if len(teile) > 2 and teile[2] else "Allgemein" + return cls(name=name, preis=preis, kategorie=kategorie) + + @staticmethod + def ist_gueltiger_preis(preis: Any) -> bool: + """ + Prüft statisch, ob ein Preis gültig (numerisch und >= 0) ist. + """ + if isinstance(preis, (int, float)) and not isinstance(preis, bool): + return preis >= 0.0 + return False + + +@dataclass +class Warenkorb: + """ + Ein digitaler Warenkorb, der Artikel sammelt und verwaltet. + """ + kunde: str + artikel_liste: List[Artikel] = field(default_factory=list) + + def artikel_hinzufuegen(self, *artikel: Artikel) -> None: + """ + Fügt beliebig viele Artikel (*args) zum Warenkorb hinzu. + """ + for a in artikel: + if not isinstance(a, Artikel): + raise TypeError(f"Erwartete Artikel-Instanz, erhielt: {type(a).__name__}") + self.artikel_liste.append(a) + + def artikel_entfernen(self, artikel_name: str) -> bool: + """ + Entfernt den ersten Artikel mit dem passenden Namen (Groß-/Kleinschreibung ignorieren). + """ + such_name = artikel_name.strip().lower() + for a in self.artikel_liste: + if a.name.strip().lower() == such_name: + self.artikel_liste.remove(a) + return True + return False + + def gesamtsumme(self, brutto: bool = False, mwst_satz: float = 0.19) -> float: + """ + Berechnet die Gesamtsumme aller Artikel im Warenkorb. + """ + if not self.artikel_liste: + return 0.0 + + if brutto: + summe = sum(a.berechne_bruttopreis(mwst_satz) for a in self.artikel_liste) + else: + summe = sum(a.preis for a in self.artikel_liste) + + return round(summe, 2) + + def __len__(self) -> int: + return len(self.artikel_liste) + + +# ============================================================================== +# TEIL 2: EIGENE DECORATORS SCHREIBEN +# ============================================================================== + +def aufruf_zaehler(func: Callable) -> Callable: + """ + Ein Decorator, der zählt, wie oft eine dekorierte Funktion aufgerufen wurde. + """ + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + wrapper.aufrufe += 1 + return func(*args, **kwargs) + + wrapper.aufrufe = 0 + return wrapper + + +def zeitmessung(func: Callable) -> Callable: + """ + Ein Decorator, der die Ausführungszeit einer Funktion misst. + """ + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + start = time.perf_counter() + ergebnis = func(*args, **kwargs) + dauer = time.perf_counter() - start + wrapper.letzte_dauer = dauer + return ergebnis + + wrapper.letzte_dauer = 0.0 + return wrapper + + +# ============================================================================== +# TEIL 3: FORTGESCHRITTENE TYPE HINTS (UNION, DICT, TYPED DATA) +# ============================================================================== + +def formatiere_benutzer(daten: Dict[str, Union[str, int, bool]]) -> str: + """ + Formatiert ein Benutzer-Dictionary mit verschiedenen Datentypen zu einem + lesbaren Status-String. + """ + if "username" not in daten and "name" not in daten: + raise KeyError("Benutzerdaten müssen 'username' oder 'name' enthalten!") + + username_val = daten.get("username", daten.get("name")) + if not username_val or not str(username_val).strip(): + raise ValueError("Benutzername darf nicht leer sein!") + + username = str(username_val).strip() + + alter_val = daten.get("alter") + alter_str = str(alter_val) if alter_val is not None else "Unbekannt" + + ist_admin = daten.get("ist_admin", False) + rolle = "Administrator" if ist_admin else "Standardbenutzer" + + return f"Benutzer: {username} | Alter: {alter_str} | Rolle: {rolle}" + + +# ============================================================================== +# DEMONSTRATION & MAIN +# ============================================================================== + +if __name__ == "__main__": + print("=" * 70) + print("🏷️ KAPITEL 20: MUSTERLÖSUNG – DATACLASSES & DECORATORS") + print("=" * 70) + + # 1. Dataclass Artikel testen + laptop = Artikel(name="ThinkPad X1", preis=1499.00, kategorie="Elektronik") + print(f"\n📦 Artikel Dataclass:\n {laptop}") + print(f" Bruttopreis (19%): {laptop.berechne_bruttopreis(0.19):.2f} €") + + # 2. CSV Fabrikmethode + maus = Artikel.aus_csv_zeile("Logitech MX Master;99.90;Zubehoer") + print(f" Aus CSV erstellt: {maus}") + + # 3. Warenkorb testen + korb = Warenkorb(kunde="Max Mustermann") + korb.artikel_hinzufuegen(laptop, maus, Artikel("Kaffeebecher", 12.50, "Haushalt")) + print(f"\n🛒 Warenkorb für {korb.kunde} ({len(korb)} Artikel):") + print(f" Nettosumme: {korb.gesamtsumme():.2f} €") + print(f" Bruttosumme: {korb.gesamtsumme(brutto=True):.2f} €") + + # 4. Aufruf-Zähler Decorator + @aufruf_zaehler + def sage_hallo(name: str) -> str: + return f"Hallo, {name}!" + + print("\n🔢 Aufruf-Zähler Decorator Test:") + print(sage_hallo("Alice")) + print(sage_hallo("Bob")) + print(sage_hallo("Charlie")) + print(f" Anzahl Aufrufe: {sage_hallo.aufrufe}") + + # 5. Typsicherer Benutzer-Formatierer + user = {"username": "CoderGirl99", "alter": 24, "ist_admin": True} + print(f"\n👤 Benutzerdaten:\n {formatiere_benutzer(user)}") + + print("\n" + "=" * 70) + print("✅ Musterlösung erfolgreich ausgeführt!") + print("=" * 70) diff --git a/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/test_aufgabe.py new file mode 100644 index 0000000..f93e904 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints/test_aufgabe.py @@ -0,0 +1,191 @@ +import unittest +import sys +import time +from pathlib import Path +from dataclasses import is_dataclass + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel20(unittest.TestCase): + """ + Testsuite für Kapitel 20: Dataclasses, Type Hints & Decorators. + """ + + def setUp(self): + self.Artikel = getattr(aufgabe, "Artikel", None) + self.Warenkorb = getattr(aufgabe, "Warenkorb", None) + self.aufruf_zaehler = getattr(aufgabe, "aufruf_zaehler", None) + self.zeitmessung = getattr(aufgabe, "zeitmessung", None) + self.formatiere_benutzer = getattr(aufgabe, "formatiere_benutzer", None) + + self.assertIsNotNone(self.Artikel, "Klasse 'Artikel' nicht gefunden!") + self.assertIsNotNone(self.Warenkorb, "Klasse 'Warenkorb' nicht gefunden!") + self.assertIsNotNone(self.aufruf_zaehler, "Decorator 'aufruf_zaehler' nicht gefunden!") + self.assertIsNotNone(self.zeitmessung, "Decorator 'zeitmessung' nicht gefunden!") + self.assertIsNotNone(self.formatiere_benutzer, "Funktion 'formatiere_benutzer' nicht gefunden!") + + # ========================================================================== + # 1. Dataclass Artikel + # ========================================================================== + def test_01_artikel_dataclass_basis(self): + """Prüft, ob Artikel eine echte Dataclass ist und Standardwerte setzt.""" + self.assertTrue(is_dataclass(self.Artikel), "Artikel muss mit @dataclass dekoriert sein!") + + a = self.Artikel("Buch", 19.99) + self.assertEqual(a.name, "Buch") + self.assertEqual(a.preis, 19.99) + self.assertEqual(a.kategorie, "Allgemein") + + # Gleichheit und Repräsentation (Dataclass-Features) + a2 = self.Artikel("Buch", 19.99) + self.assertEqual(a, a2, "Zwei Artikel mit identischen Werten müssen gleich (==) sein!") + self.assertIn("Buch", repr(a)) + + def test_02_artikel_validierung(self): + """Prüft die Validierung in __post_init__.""" + with self.assertRaises(ValueError, msg="Negativer Preis muss ValueError auslösen!"): + self.Artikel("Test", -5.0) + + with self.assertRaises(ValueError, msg="Leerer Name muss ValueError auslösen!"): + self.Artikel("", 10.0) + + with self.assertRaises(ValueError, msg="Nur-Leerzeichen-Name muss ValueError auslösen!"): + self.Artikel(" ", 10.0) + + def test_03_artikel_berechne_bruttopreis(self): + """Prüft die Bruttoberechnung mit verschiedenen Steuersätzen.""" + buch = self.Artikel("Buch", 10.0) + self.assertEqual(buch.berechne_bruttopreis(0.07), 10.70) + self.assertEqual(buch.berechne_bruttopreis(0.19), 11.90) + self.assertEqual(buch.berechne_bruttopreis(0.0), 10.0) + + with self.assertRaises(ValueError, msg="Negativer Steuersatz muss ValueError werfen"): + buch.berechne_bruttopreis(-0.1) + + def test_04_artikel_classmethod_und_staticmethod(self): + """Prüft @classmethod aus_csv_zeile und @staticmethod ist_gueltiger_preis.""" + # Classmethod + a = self.Artikel.aus_csv_zeile("Monitor;249.99;Hardware") + self.assertEqual(a.name, "Monitor") + self.assertEqual(a.preis, 249.99) + self.assertEqual(a.kategorie, "Hardware") + + # Staticmethod + self.assertTrue(self.Artikel.ist_gueltiger_preis(100)) + self.assertTrue(self.Artikel.ist_gueltiger_preis(0.0)) + self.assertFalse(self.Artikel.ist_gueltiger_preis(-10)) + self.assertFalse(self.Artikel.ist_gueltiger_preis("100")) + + # ========================================================================== + # 2. Dataclass Warenkorb + # ========================================================================== + def test_05_warenkorb_basis_und_default_factory(self): + """Prüft Warenkorb, default_factory und Unabhängigkeit von Instanzen.""" + self.assertTrue(is_dataclass(self.Warenkorb), "Warenkorb muss mit @dataclass dekoriert sein!") + + k1 = self.Warenkorb(kunde="Alice") + k2 = self.Warenkorb(kunde="Bob") + + self.assertEqual(len(k1), 0) + self.assertEqual(k1.gesamtsumme(), 0.0) + + # Sicherstellen, dass default_factory=list genutzt wurde (keine geteilte Referenz) + k1.artikel_hinzufuegen(self.Artikel("Tee", 3.50)) + self.assertEqual(len(k1), 1) + self.assertEqual(len(k2), 0, "artikel_liste darf nicht zwischen Warenkörben geteilt werden!") + + def test_06_warenkorb_artikel_hinzufuegen_und_entfernen(self): + """Prüft Hinzufügen (*args), Typprüfung und Entfernen von Artikeln.""" + korb = self.Warenkorb(kunde="Charlie") + a1 = self.Artikel("Kaffee", 6.00) + a2 = self.Artikel("Keks", 2.00) + + korb.artikel_hinzufuegen(a1, a2) + self.assertEqual(len(korb), 2) + self.assertEqual(korb.gesamtsumme(), 8.00) + self.assertEqual(korb.gesamtsumme(brutto=True, mwst_satz=0.10), 8.80) + + # Typprüfung + with self.assertRaises(TypeError, msg="Ungültige Objekte müssen TypeError auslösen!"): + korb.artikel_hinzufuegen("Kein Artikel") + + # Artikel entfernen + entfernt = korb.artikel_entfernen("kaffee") # Case-insensitive + self.assertTrue(entfernt) + self.assertEqual(len(korb), 1) + self.assertEqual(korb.gesamtsumme(), 2.00) + + # Nicht vorhandener Artikel + self.assertFalse(korb.artikel_entfernen("NichtDa")) + + # ========================================================================== + # 3. Decorator aufruf_zaehler + # ========================================================================== + def test_07_aufruf_zaehler_decorator(self): + """Prüft, ob der Decorator Aufrufe zählt und functools.wraps verwendet.""" + @self.aufruf_zaehler + def addiere(a: int, b: int) -> int: + """Addiert zwei Zahlen.""" + return a + b + + self.assertEqual(addiere.__name__, "addiere", "functools.wraps muss den Funktionsnamen bewahren!") + self.assertEqual(addiere.__doc__, "Addiert zwei Zahlen.", "functools.wraps muss den Docstring bewahren!") + self.assertEqual(addiere.aufrufe, 0) + + res1 = addiere(5, 10) + self.assertEqual(res1, 15) + self.assertEqual(addiere.aufrufe, 1) + + res2 = addiere(20, 30) + self.assertEqual(res2, 50) + self.assertEqual(addiere.aufrufe, 2) + + # ========================================================================== + # 4. Decorator zeitmessung + # ========================================================================== + def test_08_zeitmessung_decorator(self): + """Prüft, ob zeitmessung die Dauer erfasst und das Ergebnis zurückgibt.""" + @self.zeitmessung + def schlafe_kurz(): + time.sleep(0.01) + return "Aufgewacht" + + ergebnis = schlafe_kurz() + self.assertEqual(ergebnis, "Aufgewacht") + self.assertTrue(hasattr(schlafe_kurz, "letzte_dauer")) + self.assertGreaterEqual(schlafe_kurz.letzte_dauer, 0.005) + + # ========================================================================== + # 5. Type Hints formatiere_benutzer + # ========================================================================== + def test_09_formatiere_benutzer(self): + """Prüft die Formatierung typisierter Benutzer-Dictionaries und Fehler.""" + u1 = {"username": "Alice", "alter": 30, "ist_admin": True} + res1 = self.formatiere_benutzer(u1) + self.assertIn("Alice", res1) + self.assertIn("30", res1) + self.assertIn("Administrator", res1) + + u2 = {"username": "Bob", "ist_admin": False} + res2 = self.formatiere_benutzer(u2) + self.assertIn("Bob", res2) + self.assertIn("Unbekannt", res2) + self.assertIn("Standardbenutzer", res2) + + # Fehler bei fehlendem Username + with self.assertRaises(KeyError): + self.formatiere_benutzer({"alter": 20}) + + with self.assertRaises(ValueError): + self.formatiere_benutzer({"username": " "}) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/README.md b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/README.md new file mode 100644 index 0000000..c440123 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/README.md @@ -0,0 +1,48 @@ +# Kapitel 21: Relationale Datenbanken & SQLite (sqlite3) 🗄️⚡ + +In diesem Kapitel lernst du, wie du strukturierte relationale Datenbanken mit Pythons eingebautem Modul `sqlite3` erstellst, verwaltest und sicher abfragst. + +--- + +## 🎯 Was du lernst + +1. **Relationale Datenbanken vs. Einfache Textdateien:** Warum relationale Datenbanken (Tabellen, Primärschlüssel, Relationen) für große Datenmengen und gleichzeitigen Zugriff unverzichtbar sind. +2. **Die Aktenschrank-Analogie:** + - **Datenbankdatei:** Der Aktenschrank. + - **Tabelle:** Eine Schublade mit Registerkarten. + - **Spalte (Column):** Ein vorgegebenes Feld auf der Registerkarte (z.B. Name, E-Mail, Guthaben). + - **Zeile (Row/Record):** Eine einzelne ausgefüllte Registerkarte (ein Kunde). +3. **Verbindung & Cursor:** + ```python + import sqlite3 + + conn = sqlite3.connect("kunden.db") # oder ":memory:" für flüchtige RAM-Datenbank + conn.row_factory = sqlite3.Row # Zugriff per Spaltennamen wie im Dict! + cursor = conn.cursor() + ``` +4. **CRUD-Operationen in SQL:** + - **C**reate: `CREATE TABLE IF NOT EXISTS ...` und `INSERT INTO ...` + - **R**ead: `SELECT ... FROM ... WHERE ...` + - **U**pdate: `UPDATE ... SET ... WHERE ...` + - **D**elete: `DELETE FROM ... WHERE ...` +5. **Transaktionen & Persistenz:** Warum Änderungen erst nach `conn.commit()` dauerhaft auf der Festplatte gespeichert werden. +6. **Sicherheit & SQL-Injection-Schutz:** Niemals Strings formatieren (`f"SELECT ... {user_input}"`), sondern immer Platzhalter (`?`) verwenden! + +--- + +## 📁 Die Dateien in diesem Ordner + +- **`index.html`**: Die interaktive Lernseite mit Aktenschrank-Analogie, SQL-Cheat-Sheet, Injection-Demo und Subgoals. +- **`aufgabe.py`**: Dein Arbeitsblatt mit der Klasse `KundenDatenbank`. +- **`test_aufgabe.py`**: Automatische Unittest-Suite (`python3 test_aufgabe.py`). +- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Teste deine Lösung: +python3 test_aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/aufgabe.py b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/aufgabe.py new file mode 100644 index 0000000..e88d599 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/aufgabe.py @@ -0,0 +1,169 @@ +""" +Kapitel 21: Relationale Datenbanken & SQLite (sqlite3) 🗄️⚡ +========================================================= +Lehrpfad 4: Professional Data Engineering + +Thema: +Relationales Datenmanagement mit SQLite und Pythons eingebautem Modul `sqlite3`. + +Lernziele: +1. Verbindungsaufbau mit `sqlite3.connect()` und Arbeiten mit dem Cursor `conn.cursor()`. +2. Tabellen definieren mit `CREATE TABLE IF NOT EXISTS`. +3. Datensätze einfügen mit `INSERT INTO` und `conn.commit()`. +4. Daten gezielt abfragen mit `SELECT ... WHERE` und `fetchall()` / `fetchone()`. +5. Datensätze aktualisieren mit `UPDATE ... SET` und löschen mit `DELETE FROM`. +6. Schutz vor SQL-Injections durch parametrisierte Abfragen (`?`). +7. Verwendung von `row_factory = sqlite3.Row` für benannte Spaltenzugriffe. +""" + +import sqlite3 +from typing import List, Optional, Dict, Any + + +class KundenDatenbank: + """ + Verwaltet Kundenkonten und Guthaben in einer relationalen SQLite-Datenbank. + """ + + def __init__(self, db_pfad: str = ":memory:"): + """ + Initialisiert die Datenbankverbindung und erstellt die Tabelle 'kunden', + falls sie noch nicht existiert. + + Tabelle 'kunden': + - id: INTEGER PRIMARY KEY AUTOINCREMENT + - name: TEXT NOT NULL + - email: TEXT NOT NULL UNIQUE + - guthaben: REAL DEFAULT 0.0 + + Tipps: + - self.db_pfad = db_pfad + - self.conn = sqlite3.connect(db_pfad) + - self.conn.row_factory = sqlite3.Row # Ermöglicht dict-artigen Zugriff + - self.cursor = self.conn.cursor() + - Führe den CREATE TABLE Befehl mit self.cursor.execute(...) aus + - Führe self.conn.commit() aus + """ + # TODO: Implementieren + pass + + def kunde_hinzufuegen(self, name: str, email: str, startguthaben: float = 0.0) -> int: + """ + Fügt einen neuen Kunden in die Datenbank ein. + + WICHTIG: + - Verwende IMMER parametrisierte SQL-Queries mit '?' gegen SQL-Injections! + - SQL: "INSERT INTO kunden (name, email, guthaben) VALUES (?, ?, ?)" + - Führe nach dem INSERT self.conn.commit() auf. + + Rückgabewert: + - Die automatisch vergebene ID des neuen Kunden (int) via self.cursor.lastrowid + """ + # TODO: Implementieren + pass + + def kunde_suchen_nach_id(self, kunden_id: int) -> Optional[Dict[str, Any]]: + """ + Sucht einen Kunden anhand seiner ID. + + SQL: + "SELECT id, name, email, guthaben FROM kunden WHERE id = ?" + + Rückgabewert: + - Ein Dictionary mit den Spalten als Keys: + {"id": row["id"], "name": row["name"], "email": row["email"], "guthaben": row["guthaben"]} + - None, falls kein Kunde mit dieser ID gefunden wurde. + """ + # TODO: Implementieren + pass + + def guthaben_aufladen(self, kunden_id: int, betrag: float) -> bool: + """ + Lädt das Guthaben eines bestehenden Kunden um den angegebenen Betrag auf. + + Bedingungen: + - betrag muss größer als 0 sein (andernfalls direkt False zurückgeben). + - SQL: "UPDATE kunden SET guthaben = guthaben + ? WHERE id = ?" + - Führe self.conn.commit() aus. + + Rückgabewert: + - True, wenn der Datensatz aktualisiert wurde (self.cursor.rowcount > 0). + - False, wenn kein Kunde mit dieser ID existiert oder betrag <= 0 ist. + """ + # TODO: Implementieren + pass + + def kunden_mit_mindestguthaben(self, min_guthaben: float) -> List[Dict[str, Any]]: + """ + Gibt alle Kunden zurück, deren Guthaben >= min_guthaben ist. + Sortiert nach Guthaben absteigend (höchstes Guthaben zuerst). + + SQL: + "SELECT id, name, email, guthaben FROM kunden WHERE guthaben >= ? ORDER BY guthaben DESC" + + Rückgabewert: + - Liste von Dictionaries der passenden Kunden. + [{"id": ..., "name": ..., "email": ..., "guthaben": ...}, ...] + """ + # TODO: Implementieren + pass + + def kunde_loeschen(self, kunden_id: int) -> bool: + """ + Löscht einen Kunden anhand seiner ID aus der Datenbank. + + SQL: + "DELETE FROM kunden WHERE id = ?" + + Rückgabewert: + - True, wenn ein Kunde gelöscht wurde (self.cursor.rowcount > 0). + - False, wenn kein Kunde mit dieser ID existierte. + """ + # TODO: Implementieren + pass + + def alle_kunden(self) -> List[Dict[str, Any]]: + """ + Gibt eine Liste aller Kunden zurück. + """ + # TODO: Implementieren + pass + + def schliessen(self) -> None: + """Schließt die Datenbankverbindung.""" + if hasattr(self, "conn") and self.conn: + self.conn.close() + + +# ============================================================================== +# Interaktiver Test im Terminal: +# (python3 aufgabe.py) +# ============================================================================== +if __name__ == "__main__": + print("🗄️ Initialisiere KundenDatenbank im Arbeitsspeicher (:memory:)...") + db = KundenDatenbank(":memory:") + + # 1. Kunden anlegen + id1 = db.kunde_hinzufuegen("Alice Schmidt", "alice@example.com", 150.0) + id2 = db.kunde_hinzufuegen("Bob Müller", "bob@example.com", 50.0) + id3 = db.kunde_hinzufuegen("Charlie Braun", "charlie@example.com", 320.0) + print(f"Kunden erstellt: IDs = {id1}, {id2}, {id3}") + + # 2. Kunde suchen + kunde = db.kunde_suchen_nach_id(id1) + print(f"Gefundener Kunde: {kunde}") + + # 3. Guthaben aufladen + erfolg = db.guthaben_aufladen(id2, 100.0) + print(f"Guthaben von Bob aufgeladen: {erfolg} -> Neuer Stand: {db.kunde_suchen_nach_id(id2)}") + + # 4. Mindestguthaben filtern + reiche_kunden = db.kunden_mit_mindestguthaben(140.0) + print(f"Kunden mit mind. 140€ Guthaben: {reiche_kunden}") + + # 5. Kunde löschen + db.kunde_loeschen(id3) + print(f"Nach Löschen von Charlie: {db.alle_kunden()}") + + db.schliessen() + print("Fertig!") diff --git a/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/index.html b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/index.html new file mode 100644 index 0000000..c5aff6f --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/index.html @@ -0,0 +1,502 @@ + + + + + + Kapitel 21: Relationale Datenbanken & SQLite + + + + +
+ + +
+
+
+ +
Kapitel 21 • Data Engineering • SQL & Persistenz
+

Relationale Datenbanken & SQLite 🗄️⚡

+

Lerne die Grundlagen relationaler Datenbanken mit Pythons integriertem sqlite3-Modul, meistere CRUD-Operationen und schütze deine Daten vor SQL-Injections.

+
+
+ +
+ + + + + +
+

1. Was ist eine relationale Datenbank? (Die Aktenschrank-Analogie)

+ +

+ In vorherigen Kapiteln haben wir Daten in einfachen JSON- oder CSV-Dateien gespeichert. Bei größeren Anwendungen stoßen Textdateien jedoch schnell an ihre Grenzen: + Man kann nicht blitzschnell nach einzelnen Datensätzen suchen, ohne die gesamte Datei einzulesen, und gleichzeitige Schreibzugriffe führen zu Datenverlust. + Hier kommen relationale Datenbanksysteme (RDBMS) ins Spiel! +

+ +
+
🗄️ Die Aktenschrank-Analogie:
+ Stell dir eine relationale Datenbank wie einen soliden Büroschrank vor: +
    +
  • Die Datenbankdatei (.db): Der gesamte Aktenschrank im Büro.
  • +
  • Die Tabelle (Table): Eine einzelne ausziehbare Schublade (z.B. Schublade „Kunden“).
  • +
  • Die Spalte (Column / Field): Ein vorgedrucktes Formularfeld auf den Karteikarten (z.B. „Name“, „E-Mail“, „Guthaben“).
  • +
  • Die Zeile (Row / Record): Eine einzelne, ausgefüllte Karteikarte für genau einen Kunden.
  • +
  • Der Primärschlüssel (PRIMARY KEY): Die eindeutige Kundennummer oben rechts auf jeder Karteikarte.
  • +
+
+ +

Der Lebenszyklus einer SQLite-Verbindung in Python

+

+ Python bringt mit sqlite3 eine vollständige, vollwertige SQL-Datenbank direkt in der Standardbibliothek mit – ohne Server-Installation! +

+ +
+
+ Grundgerüst mit sqlite3 +
+
import sqlite3
+
+# 1. Verbindung herstellen (erstellt die Datei automatisch, falls nicht vorhanden)
+conn = sqlite3.connect("kunden.db")
+
+# 2. row_factory aktivieren: Erlaubt Dict-artigen Spaltenzugriff wie row["name"]
+conn.row_factory = sqlite3.Row
+
+# 3. Cursor erzeugen (der 'Stift', mit dem wir Befehle an die DB schicken)
+cursor = conn.cursor()
+
+# 4. SQL-Befehl ausführen
+cursor.execute("""
+    CREATE TABLE IF NOT EXISTS kunden (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        name TEXT NOT NULL,
+        email TEXT NOT NULL UNIQUE,
+        guthaben REAL DEFAULT 0.0
+    )
+""")
+
+# 5. Transaktion dauerhaft speichern (Commit)
+conn.commit()
+
+# 6. Verbindung sauber schließen
+conn.close()
+
+
+ + +
+

2. Die 4 Säulen von SQL: CRUD im Überblick

+

In relationalen Datenbanken dreht sich alles um die vier elementaren CRUD-Operationen:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationSQL-BefehlPython-Beispiel mit Platzhaltern (?)
Create (Erstellen)INSERT INTO ...cursor.execute("INSERT INTO kunden (name, email, guthaben) VALUES (?, ?, ?)", (name, email, 50.0))
Read (Lesen / Suchen)SELECT ... WHEREcursor.execute("SELECT * FROM kunden WHERE id = ?", (kunden_id,))
row = cursor.fetchone()
Update (Aktualisieren)UPDATE ... SETcursor.execute("UPDATE kunden SET guthaben = guthaben + ? WHERE id = ?", (betrag, kunden_id))
Delete (Löschen)DELETE FROM ...cursor.execute("DELETE FROM kunden WHERE id = ?", (kunden_id,))
+ +
+
💡 Cursor-Methoden im Detail:
+
    +
  • cursor.fetchone(): Liefert die nächste einzelne Ergebniszeile (oder None).
  • +
  • cursor.fetchall(): Liefert eine Liste aller passenden Ergebniszeilen (oder []).
  • +
  • cursor.lastrowid: Die automatisch generierte ID des zuletzt eingefügten Datensatzes.
  • +
  • cursor.rowcount: Die Anzahl der Zeilen, die durch das letzte UPDATE oder DELETE verändert wurden.
  • +
+
+
+ + +
+

3. 🚨 Sicherheit: Schutz vor SQL-Injections mit ?-Parametern

+ +

+ Eine der häufigsten und gefährlichsten Sicherheitslücken in der Software-Entwicklung ist die SQL-Injection. + Sie entsteht, wenn Benutzereingaben unbedacht per String-Formatierung (z.B. f-Strings) direkt in den SQL-Befehl eingebettet werden! +

+ +
+
+

❌ GEFÄHRLICH: String-Formatierung

+
# Angreifer gibt als Name ein:
+# Robert'); DROP TABLE kunden;--
+
+query = f"INSERT INTO kunden (name) VALUES ('{user_name}')"
+cursor.execute(query)
+# BUMM! Die Tabelle kunden wird gelöscht!
+
+ +
+

✅ SICHER: Parametrisierte Abfrage

+
# Das sqlite3-Modul maskiert alle Zeichen:
+
+query = "INSERT INTO kunden (name) VALUES (?)"
+cursor.execute(query, (user_name,))
+
+# Der Name wird harmlos als Text gespeichert!
+
+
+
+ + +
+

4. In-Memory Datenbanken mit :memory:

+

+ Für automatisierte Unit Tests oder temporäre Berechnungen bietet SQLite ein geniales Feature: die In-Memory-Datenbank. +

+
+
conn = sqlite3.connect(":memory:")
+
+

+ Die Datenbank existiert ausschließlich im flüchtigen RAM. Sobald conn.close() aufgerufen wird oder der Test endet, ist alles rückstandslos bereinigt – + ideal für blitzschnelle, voneinander isolierte Tests ohne lästige temporäre Testdateien auf der Festplatte! +

+
+ + +
+

5. 🎯 Deine Aufgaben in aufgabe.py

+ +

Öffne die Datei aufgabe.py und implementiere die Klasse KundenDatenbank:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zur Tabellenerstellung +
+
+
self.cursor.execute("""
+    CREATE TABLE IF NOT EXISTS kunden (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        name TEXT NOT NULL,
+        email TEXT NOT NULL UNIQUE,
+        guthaben REAL DEFAULT 0.0
+    )
+""")
+self.conn.commit()
+
+
+
+ +
+ 🔍 Tipp zu row_factory & Dict-Rückgabe +
+
+
self.cursor.execute("SELECT id, name, email, guthaben FROM kunden WHERE id = ?", (kunden_id,))
+row = self.cursor.fetchone()
+if row is None:
+    return None
+return {
+    "id": row["id"],
+    "name": row["name"],
+    "email": row["email"],
+    "guthaben": float(row["guthaben"])
+}
+
+
+
+
+ + +
+

6. 🧪 Lösung überprüfen

+

Führe im Terminal im Ordner 21_datenbanken_und_sqlite folgenden Befehl aus:

+
+
+ Terminal +
+
python3 test_aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/musterloesung.py b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/musterloesung.py new file mode 100644 index 0000000..eb1e93b --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/musterloesung.py @@ -0,0 +1,140 @@ +""" +Kapitel 21: Relationale Datenbanken & SQLite (sqlite3) 🗄️⚡ +========================================================= +Lehrpfad 4: Professional Data Engineering + +Musterlösung für die KundenDatenbank mit SQLite3. +""" + +import sqlite3 +from typing import List, Optional, Dict, Any + + +class KundenDatenbank: + """ + Vollständig implementierte KundenDatenbank mit sqlite3. + """ + + def __init__(self, db_pfad: str = ":memory:"): + """ + Initialisiert die Datenbankverbindung und erstellt die Tabelle 'kunden'. + """ + self.db_pfad = db_pfad + self.conn = sqlite3.connect(db_pfad) + self.conn.row_factory = sqlite3.Row + self.cursor = self.conn.cursor() + + # Tabelle anlegen + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS kunden ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + guthaben REAL DEFAULT 0.0 + ) + """ + ) + self.conn.commit() + + def kunde_hinzufuegen(self, name: str, email: str, startguthaben: float = 0.0) -> int: + """ + Fügt einen neuen Kunden per parametrisiertem INSERT ein. + """ + self.cursor.execute( + "INSERT INTO kunden (name, email, guthaben) VALUES (?, ?, ?)", + (name, email, float(startguthaben)) + ) + self.conn.commit() + return self.cursor.lastrowid + + def kunde_suchen_nach_id(self, kunden_id: int) -> Optional[Dict[str, Any]]: + """ + Sucht einen Kunden anhand der ID und liefert ein Dictionary zurück. + """ + self.cursor.execute( + "SELECT id, name, email, guthaben FROM kunden WHERE id = ?", + (kunden_id,) + ) + row = self.cursor.fetchone() + if row is None: + return None + return { + "id": row["id"], + "name": row["name"], + "email": row["email"], + "guthaben": float(row["guthaben"]), + } + + def guthaben_aufladen(self, kunden_id: int, betrag: float) -> bool: + """ + Erhöht das Guthaben eines Kunden um einen positiven Betrag. + """ + if betrag <= 0: + return False + + self.cursor.execute( + "UPDATE kunden SET guthaben = guthaben + ? WHERE id = ?", + (float(betrag), kunden_id) + ) + self.conn.commit() + return self.cursor.rowcount > 0 + + def kunden_mit_mindestguthaben(self, min_guthaben: float) -> List[Dict[str, Any]]: + """ + Liefert alle Kunden mit mindestens min_guthaben, sortiert absteigend. + """ + self.cursor.execute( + "SELECT id, name, email, guthaben FROM kunden WHERE guthaben >= ? ORDER BY guthaben DESC", + (float(min_guthaben),) + ) + rows = self.cursor.fetchall() + return [ + { + "id": r["id"], + "name": r["name"], + "email": r["email"], + "guthaben": float(r["guthaben"]), + } + for r in rows + ] + + def kunde_loeschen(self, kunden_id: int) -> bool: + """ + Löscht einen Kunden anhand der ID. + """ + self.cursor.execute( + "DELETE FROM kunden WHERE id = ?", + (kunden_id,) + ) + self.conn.commit() + return self.cursor.rowcount > 0 + + def alle_kunden(self) -> List[Dict[str, Any]]: + """ + Gibt eine Liste aller Kunden zurück. + """ + self.cursor.execute("SELECT id, name, email, guthaben FROM kunden ORDER BY id ASC") + rows = self.cursor.fetchall() + return [ + { + "id": r["id"], + "name": r["name"], + "email": r["email"], + "guthaben": float(r["guthaben"]), + } + for r in rows + ] + + def schliessen(self) -> None: + """Schließt die Verbindung.""" + if hasattr(self, "conn") and self.conn: + self.conn.close() + + +if __name__ == "__main__": + db = KundenDatenbank(":memory:") + i1 = db.kunde_hinzufuegen("Max Mustermann", "max@test.de", 100.0) + print("Kunde hinzugefügt mit ID:", i1) + print("Kunde gesucht:", db.kunde_suchen_nach_id(i1)) + db.schliessen() diff --git a/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/test_aufgabe.py new file mode 100644 index 0000000..84cf9ec --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite/test_aufgabe.py @@ -0,0 +1,156 @@ +import unittest +import sys +import sqlite3 +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel21(unittest.TestCase): + """ + Umfassende Testsuite für Modul 21 (SQLite Datenbanken & Persistenz). + Nutzt isolierte In-Memory-Datenbanken (:memory:) für maximale Geschwindigkeit. + """ + + def setUp(self): + self.KundenDatenbank = getattr(aufgabe, "KundenDatenbank", None) + self.assertIsNotNone(self.KundenDatenbank, "Klasse 'KundenDatenbank' nicht in aufgabe.py gefunden!") + # Jede Testmethode erhält eine frische, isolierte In-Memory-Datenbank + self.db = self.KundenDatenbank(":memory:") + + def tearDown(self): + if hasattr(self, "db") and self.db: + self.db.schliessen() + + def test_01_tabelle_und_initialisierung(self): + """Prüft, ob die Tabelle 'kunden' mit den korrekten Spalten angelegt wurde.""" + cursor = self.db.conn.cursor() + cursor.execute("PRAGMA table_info(kunden)") + spalten = {row[1]: row[2].upper() for row in cursor.fetchall()} + + self.assertIn("id", spalten, "Spalte 'id' fehlt in Tabelle 'kunden'!") + self.assertIn("name", spalten, "Spalte 'name' fehlt in Tabelle 'kunden'!") + self.assertIn("email", spalten, "Spalte 'email' fehlt in Tabelle 'kunden'!") + self.assertIn("guthaben", spalten, "Spalte 'guthaben' fehlt in Tabelle 'kunden'!") + + def test_02_kunde_hinzufuegen(self): + """Prüft das Einfügen von Kunden und die Rückgabe der neuen ID.""" + id1 = self.db.kunde_hinzufuegen("Alice Schmidt", "alice@example.com", 150.0) + self.assertIsInstance(id1, int, "kunde_hinzufuegen muss die int-ID zurückgeben!") + self.assertEqual(id1, 1, "Der erste Kunde sollte ID 1 erhalten!") + + id2 = self.db.kunde_hinzufuegen("Bob Müller", "bob@example.com", 75.50) + self.assertEqual(id2, 2, "Der zweite Kunde sollte ID 2 erhalten!") + + # Prüfen, ob Datensätze direkt in der DB liegen + c = self.db.conn.cursor() + c.execute("SELECT COUNT(*) FROM kunden") + anzahl = c.fetchone()[0] + self.assertEqual(anzahl, 2, "Es sollten genau 2 Kunden in der Datenbank vorhanden sein!") + + def test_03_kunde_suchen_nach_id(self): + """Prüft die Suche nach existierenden und nicht-existierenden Kunden.""" + id_neu = self.db.kunde_hinzufuegen("Clara Oswald", "clara@tardis.org", 250.0) + + # Existierender Kunde + kunde = self.db.kunde_suchen_nach_id(id_neu) + self.assertIsInstance(kunde, dict, "kunde_suchen_nach_id muss ein Dict zurückgeben!") + self.assertEqual(kunde["id"], id_neu) + self.assertEqual(kunde["name"], "Clara Oswald") + self.assertEqual(kunde["email"], "clara@tardis.org") + self.assertAlmostEqual(kunde["guthaben"], 250.0, places=2) + + # Nicht existierender Kunde + unbekannt = self.db.kunde_suchen_nach_id(9999) + self.assertIsNone(unbekannt, "kunde_suchen_nach_id muss None zurückgeben, wenn ID nicht existiert!") + + def test_04_guthaben_aufladen(self): + """Prüft das Aufladen von Guthaben und Validierung von Beträgen.""" + kid = self.db.kunde_hinzufuegen("David Miller", "david@web.de", 100.0) + + # Gültiges Aufladen + erfolg = self.db.guthaben_aufladen(kid, 50.0) + self.assertTrue(erfolg, "guthaben_aufladen sollte True zurückgeben bei erfolgreicher Aktualisierung!") + + kunde = self.db.kunde_suchen_nach_id(kid) + self.assertAlmostEqual(kunde["guthaben"], 150.0, places=2) + + # Ungültiger Betrag (<= 0) + erfolg_negativ = self.db.guthaben_aufladen(kid, -20.0) + self.assertFalse(erfolg_negativ, "guthaben_aufladen darf bei Beträgen <= 0 keine Buchung durchführen (False)!") + + erfolg_null = self.db.guthaben_aufladen(kid, 0.0) + self.assertFalse(erfolg_null, "guthaben_aufladen darf bei Betrag 0.0 nicht erfolgreich sein!") + + # Nicht-existierende Kunden-ID + erfolg_fremd = self.db.guthaben_aufladen(8888, 50.0) + self.assertFalse(erfolg_fremd, "guthaben_aufladen sollte False zurückgeben, wenn Kunden-ID nicht existiert!") + + def test_05_kunden_mit_mindestguthaben(self): + """Prüft die Abfrage und Sortierung von Kunden nach Mindestguthaben.""" + self.db.kunde_hinzufuegen("Sparfuchs", "sparfuchs@bank.de", 10.0) + self.db.kunde_hinzufuegen("Mittelstand", "mittel@bank.de", 500.0) + self.db.kunde_hinzufuegen("Vip Kunde", "vip@bank.de", 1200.0) + self.db.kunde_hinzufuegen("Grossanleger", "investor@bank.de", 2500.0) + + # Filter ab 500.0 + ergebnis = self.db.kunden_mit_mindestguthaben(500.0) + self.assertIsInstance(ergebnis, list, "kunden_mit_mindestguthaben muss eine Liste zurückgeben!") + self.assertEqual(len(ergebnis), 3, "Es sollten genau 3 Kunden >= 500€ Guthaben haben!") + + # Prüfung der absteigenden Sortierung (Grossanleger > Vip Kunde > Mittelstand) + self.assertEqual(ergebnis[0]["name"], "Grossanleger") + self.assertEqual(ergebnis[1]["name"], "Vip Kunde") + self.assertEqual(ergebnis[2]["name"], "Mittelstand") + + # Zu hohe Schwelle -> leere Liste + keine = self.db.kunden_mit_mindestguthaben(10000.0) + self.assertEqual(keine, [], "Sollte leere Liste zurückgeben, wenn kein Kunde die Schwelle erreicht!") + + def test_06_kunde_loeschen(self): + """Prüft das Löschen von Kunden aus der Datenbank.""" + kid = self.db.kunde_hinzufuegen("Ex-Kunde", "ex@alt.de", 0.0) + + loesch_erfolg = self.db.kunde_loeschen(kid) + self.assertTrue(loesch_erfolg, "kunde_loeschen sollte True zurückgeben, wenn Kunde existierte!") + + # Erneute Suche muss None liefern + kunde = self.db.kunde_suchen_nach_id(kid) + self.assertIsNone(kunde, "Gelöschter Kunde darf über kunde_suchen_nach_id nicht mehr gefunden werden!") + + # Zweites Löschen derselben ID muss False liefern + loesch_nochmal = self.db.kunde_loeschen(kid) + self.assertFalse(loesch_nochmal, "kunde_loeschen sollte False liefern, wenn ID nicht existiert!") + + def test_07_sql_injection_schutz(self): + """Prüft, ob parametrisierte Queries vor böswilligen SQL-Injections schützen.""" + boeser_name = "Robert'); DROP TABLE kunden;--" + kid = self.db.kunde_hinzufuegen(boeser_name, "bobby@tables.com", 100.0) + + kunde = self.db.kunde_suchen_nach_id(kid) + self.assertIsNotNone(kunde, "Kunde mit SQL-Injection-String im Namen sollte als regulärer Text gespeichert werden!") + self.assertEqual(kunde["name"], boeser_name) + + # Tabelle 'kunden' muss intakt sein! + c = self.db.conn.cursor() + c.execute("SELECT COUNT(*) FROM kunden") + self.assertEqual(c.fetchone()[0], 1, "Tabelle 'kunden' wurde trotz SQL-Injection-Versuch nicht zerstört!") + + +if __name__ == "__main__": + res = unittest.main(verbosity=2, exit=False) + try: + root_dir = Path(__file__).parent.parent.parent.resolve() + sys.path.insert(0, str(root_dir)) + from gamification import report_single_chapter_result + rel_pfad = f"{Path(__file__).parent.parent.name}/{Path(__file__).parent.name}" + report_single_chapter_result(rel_pfad, res.result.wasSuccessful(), res.result.testsRun) + except Exception: + pass + sys.exit(0 if res.result.wasSuccessful() else 1) diff --git a/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/README.md b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/README.md new file mode 100644 index 0000000..93e04fd --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/README.md @@ -0,0 +1,48 @@ +# Kapitel 22: Web-APIs, REST & JSON-Feeds 🌐📡 + +In diesem Kapitel lernst du, wie moderne Web-Anwendungen über REST-APIs kommunizieren, wie du strukturierte JSON-Feeds in Python parst, verschachtelte Daten extrahierst und Fehler robust abfängst. + +--- + +## 🎯 Was du lernst + +1. **Was ist eine REST-API?** + - Client-Server-Architektur im Web: Der Client stellt eine Anfrage (**Request**) per HTTP GET/POST an den Endpunkt, der Server antwortet (**Response**). +2. **Die Kellner-Analogie:** + - **Gast (Client/Python-Skript):** Möchte Daten (z.B. Wetterbericht oder Aktienkurse) haben. + - **Kellner (API):** Nimmt die Bestellung entgegen, bringt sie in die Küche und serviert die Antwort. + - **Küche/Koch (Server/Datenbank):** Verarbeitet die Anfrage und bereitet die Rohdaten zu. + - **Speisekarte (API-Dokumentation):** Beschreibt, welche Endpunkte und Parameter erlaubt sind. +3. **JSON als Universalsprache des Internets:** + - `json.loads(json_string)`: Wandelt einen empfangenen JSON-Text in ein Python-Dictionary bzw. eine Python-Liste um. + - `json.dumps(obj, indent=2)`: Wandelt ein Python-Objekt in einen JSON-formatierten String um. +4. **HTTP Status-Codes:** + - `200 OK`: Anfrage erfolgreich beantwortet. + - `400 Bad Request`: Fehlerhafte Anfrage / ungültige Parameter. + - `404 Not Found`: Endpunkt oder Ressource nicht gefunden. + - `500 Internal Server Error`: Unerwarteter Serverfehler. +5. **Robuste Fehlerbehandlung & Defensives Parsen:** + - `json.JSONDecodeError`: Wenn der Server HTML statt JSON sendet oder der Payload unvollständig ist. + - `KeyError` / `IndexError`: Wenn erwartete Felder im JSON fehlen. + - Sicheres Auslesen mit `.get()` und Standardwerten. +6. **100% Offline- & Pyodide-kompatibel:** + - Alle Übungen und Tests nutzen realistische Mock-JSON-Strings, sodass keine Internetverbindung oder externe API-Keys benötigt werden. + +--- + +## 📁 Die Dateien in diesem Ordner + +- **`index.html`**: Die interaktive Lernseite mit Kellner-Analogie, API-Ablaufdiagramm, JSON Cheat-Sheet und Subgoals. +- **`aufgabe.py`**: Dein Arbeitsblatt mit Parser-Funktionen und der Klasse `WetterApiParser`. +- **`test_aufgabe.py`**: Automatische Unittest-Suite (`python3 test_aufgabe.py`). +- **`musterloesung.py`**: Vollständig ausprogrammierte Beispiellösung. + +--- + +## 🚀 Schnellstart + +```bash +# 1. Bearbeite aufgabe.py +# 2. Teste deine Lösung: +python3 test_aufgabe.py +``` diff --git a/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/aufgabe.py b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/aufgabe.py new file mode 100644 index 0000000..b468539 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/aufgabe.py @@ -0,0 +1,222 @@ +""" +Kapitel 22: Web-APIs, REST & JSON-Feeds 🌐📡 +============================================ +Lehrpfad 4: Professional Data Engineering + +Thema: +REST-APIs, JSON als Datenformat, strukturierte Extraktion verschachtelter Payloads +und defensive Fehlerbehandlung (KeyError, JSONDecodeError). + +Lernziele: +1. JSON-Strings parsen mit `json.loads(text)` und erzeugen mit `json.dumps(obj)`. +2. Extrahieren von Werten aus mehrfach verschachtelten Dictionaries und Listen. +3. Filtern und Transformieren von Web-Feeds (z.B. Aktienkurse, News-Ticker). +4. Validieren von HTTP-Statuscodes (200 OK vs. 404/500) und Fehler-Payloads. +5. Kapselung der API-Logik in einer wiederverwendbaren Klasse `WetterApiParser`. +""" + +import json +from typing import List, Dict, Any, Tuple, Optional, Union + + +# ============================================================================== +# 🎯 TEILZIEL 1 (TODO 1): Wetter-Antwort parsen +# ============================================================================== +def parse_wetter_antwort(json_text: str) -> Dict[str, Any]: + """ + Parst den JSON-String einer Wetter-API (z.B. OpenWeatherMap-Format) + und extrahiert die wichtigsten Kenngrößen in ein kompaktes Dictionary. + + Erwartete Eingabe-Struktur (Beispiel): + { + "name": "Wien", + "main": { + "temp": 21.5, + "humidity": 65 + }, + "weather": [ + { + "main": "Clouds", + "description": "Leicht bewölkt" + } + ] + } + + Rückgabe-Format: + { + "stadt": str, # aus data["name"] + "temperatur": float, # aus data["main"]["temp"] + "luftfeuchtigkeit": int, # aus data["main"]["humidity"] + "wetterlage": str # aus data["weather"][0]["description"] (oder data["weather"][0]["main"]) + } + + Fehlerbehandlung: + - Wenn json_text ungültiges JSON ist (json.JSONDecodeError) + oder erwartete Schlüssel fehlen (KeyError, IndexError, TypeError): + Wirf einen ValueError("Ungültige Wetterdaten: " + str(e)) + """ + # TODO: Implementieren + pass + + +# ============================================================================== +# 🎯 TEILZIEL 2 (TODO 2): Aktienkurse aus JSON-Feed filtern +# ============================================================================== +def filtriere_aktien_kurse(json_feed_text: str, min_kurs: float) -> List[Dict[str, Any]]: + """ + Parst einen Finanzmarkt-Feed und filtert alle Aktien heraus, deren Kurs + größer oder gleich `min_kurs` ist. + + Akzeptiert Formate: + 1. {"aktien": [{"symbol": "AAPL", "kurs": 185.5, "waehrung": "USD"}, ...]} + 2. oder direkt eine Liste: [{"symbol": "AAPL", "kurs": 185.5}, ...] + + Rückgabewert: + - Eine Liste von Dictionaries der gefilterten Aktien (z.B. [ {"symbol": "AAPL", "kurs": 185.5, ...}, ... ]) + - Bei ungültigem JSON oder leerem Feed: leere Liste [] zurückgeben. + """ + # TODO: Implementieren + pass + + +# ============================================================================== +# 🎯 TEILZIEL 3 (TODO 3): Schlagzeilen-Ticker formatieren +# ============================================================================== +def formatiere_nachrichten_ticker(news_json_text: str) -> str: + """ + Parst einen News-JSON-Feed und formatiert die Schlagzeilen zu einem durchgehenden + Laufband-String für einen Nachrichtenticker. + + Erwartete Eingabe-Struktur: + { + "nachrichten": [ + {"titel": "Python 3.13 veröffentlicht", "ressort": "Tech"}, + {"titel": "DAX schließt im Plus", "ressort": "Wirtschaft"} + ] + } + + Formatierungsregel: + - Jede Nachricht wird formatiert als: "[] +++ " + - Beispiel-Ergebnis: + "[Tech] Python 3.13 veröffentlicht +++ [Wirtschaft] DAX schließt im Plus +++" + - Falls keine Nachrichten vorhanden sind oder die Liste leer ist: + "+++ Keine aktuellen Eilmeldungen +++" zurückgeben. + - Bei ungültigem JSON: "+++ Fehler beim Laden des Nachrichten-Feeds +++" zurückgeben. + """ + # TODO: Implementieren + pass + + +# ============================================================================== +# 🎯 TEILZIEL 4 (TODO 4): HTTP-Antwort & Statuscode validieren +# ============================================================================== +def validiere_api_antwort(status_code: int, payload_json_text: str) -> Tuple[bool, Union[Dict[str, Any], str]]: + """ + Überprüft den HTTP-Statuscode und den JSON-Payload einer Web-API-Antwort. + + Regeln: + 1. Wenn status_code == 200: + - Versuche payload_json_text mit json.loads() zu parsen. + - Bei Erfolg: Gib (True, geparstes_dict) zurück. + - Bei json.JSONDecodeError: Gib (False, "Ungültiges JSON-Format") zurück. + + 2. Wenn status_code != 200 (z.B. 400, 404, 500): + - Versuche aus dem JSON-Payload eine Fehlermeldung zu extrahieren (Schlüssel 'error' oder 'message'). + - Falls vorhanden: Gib (False, f"HTTP Fehler {status_code}: {fehlermeldung}") zurück. + - Falls nicht parsebar oder kein Schlüssel: Gib (False, f"HTTP Fehler {status_code}") zurück. + """ + # TODO: Implementieren + pass + + +# ============================================================================== +# 🎯 TEILZIEL 5 (TODO 5): Klasse WetterApiParser +# ============================================================================== +class WetterApiParser: + """ + Objektorientierter Wrapper für Wetterdaten aus einer REST-API. + """ + + def __init__(self, raw_json: str = ""): + """ + Initialisiert den Parser. + Falls raw_json übergeben wurde, wird sofort lade_daten() ausgeführt. + """ + self.raw_json: str = raw_json + self.daten: Optional[Dict[str, Any]] = None + if raw_json: + self.lade_daten(raw_json) + + def lade_daten(self, raw_json: str) -> bool: + """ + Versucht den JSON-String mittels parse_wetter_antwort() einzulesen. + - Bei Erfolg: Speichert das extrahierte Dict in self.daten und gibt True zurück. + - Bei Fehler (ValueError): Setzt self.daten = None und gibt False zurück. + """ + # TODO: Implementieren + pass + + def get_temperatur(self) -> Optional[float]: + """Gibt die Temperatur zurück oder None, wenn keine Daten geladen sind.""" + # TODO: Implementieren + pass + + def get_stadt(self) -> Optional[str]: + """Gibt den Stadtnamen zurück oder None, wenn keine Daten geladen sind.""" + # TODO: Implementieren + pass + + def ist_frostig(self) -> bool: + """Gibt True zurück, wenn die Temperatur unter 0.0 °C liegt, sonst False.""" + # TODO: Implementieren + pass + + def ist_gueltig(self) -> bool: + """Gibt True zurück, wenn gültige Wetterdaten geladen sind (self.daten is not None).""" + # TODO: Implementieren + pass + + +# ============================================================================== +# Interaktiver Test im Terminal: +# (python3 aufgabe.py) +# ============================================================================== +if __name__ == "__main__": + beispiel_wetter = """ + { + "name": "München", + "main": {"temp": 18.2, "humidity": 72}, + "weather": [{"main": "Rain", "description": "Leichter Regen"}] + } + """ + print("🌤️ Teste parse_wetter_antwort():") + wetter = parse_wetter_antwort(beispiel_wetter) + print(wetter) + + beispiel_aktien = """ + { + "aktien": [ + {"symbol": "AAPL", "kurs": 189.5, "waehrung": "USD"}, + {"symbol": "GOOGL", "kurs": 142.0, "waehrung": "USD"}, + {"symbol": "PENNY", "kurs": 0.45, "waehrung": "USD"} + ] + } + """ + print("\n📈 Teste filtriere_aktien_kurse() (min_kurs = 100):") + gefiltert = filtriere_aktien_kurse(beispiel_aktien, 100.0) + print(gefiltert) + + beispiel_news = """ + { + "nachrichten": [ + {"titel": "Python 3.13 bringt Performance-Schub", "ressort": "Technologie"}, + {"titel": "Mars-Rover findet neue Gesteinsproben", "ressort": "Wissenschaft"} + ] + } + """ + print("\n📰 Teste formatiere_nachrichten_ticker():") + print(formatiere_nachrichten_ticker(beispiel_news)) + + print("\n🛡️ Teste validiere_api_antwort():") + print("Status 200:", validiere_api_antwort(200, '{"status": "ok", "items": [1, 2, 3]}')) + print("Status 404:", validiere_api_antwort(404, '{"error": "Ressource nicht gefunden"}')) diff --git a/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/index.html b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/index.html new file mode 100644 index 0000000..b7a91ed --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/index.html @@ -0,0 +1,443 @@ + + + + + + Kapitel 22: Web-APIs, REST & JSON-Feeds + + + + +
+ + +
+
+
+ +
Kapitel 22 • Data Engineering • Web & REST APIs
+

Web-APIs, REST & JSON-Feeds 🌐📡

+

Lerne, wie Web-APIs funktionieren, wie du strukturierte JSON-Feeds in Python parst, Daten filterst und Netzwerk- sowie Parsing-Fehler professionell abfängst.

+
+
+ +
+ + + + + +
+

1. Was ist eine REST-API? (Die Kellner-Analogie)

+ +

+ Eine API (Application Programming Interface) ist eine Schnittstelle, über die zwei Computer oder Programme übers Internet miteinander kommunizieren. + Nahezu jede moderne Web-App (wie Wetterberichte, Börsenkurse, Spotify oder Google Maps) stellt ihre Daten über REST-APIs im JSON-Format bereit. +

+ +
+
🍽️ Die Restaurant-Analogie:
+ Stell dir den Ablauf in einem Restaurant vor: +
    +
  • Der Gast (Client / Dein Python-Skript): Du sitzt am Tisch und möchtest ein bestimmtes Gericht (Daten) bestellen.
  • +
  • Die Speisekarte (API-Dokumentation / Endpunkte): Zeigt dir, welche Gerichte (URLs & Parameter wie /weather?city=Wien) du bestellen kannst.
  • +
  • Der Kellner (Die REST-API): Nimmt deine Bestellung (**HTTP Request**) entgegen, eilt in die Küche und bringt dir das fertige Essen (**HTTP Response**) zurück.
  • +
  • Die Küche & Vorratskammer (Web-Server & Datenbank): Verarbeitet die Rohzutaten und bereitet das Essen zu.
  • +
+
+ +

Der typische Ablauf eines API-Aufrufs

+
+
[Client: Python-App]
+
│  🚀 1. HTTP GET Request (z.B. https://api.wetter.com/v1/forecast?city=Berlin)
+
▼
+
[Server: Wetter-Datenbank]
+
│  📦 2. HTTP Response 200 OK + JSON-Payload: {"temp": 21.5, "city": "Berlin"}
+
▼
+
[Client: json.loads(payload)] ➔ Extrahiert Dicts, Listen und bereitet Anzeige auf
+
+
+ + +
+

2. Die wichtigsten HTTP-Statuscodes

+

Jede API-Antwort enthält einen 3-stelligen numerischen Statuscode, der signalisiert, ob die Anfrage geklappt hat:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeBedeutungTypischer Grund
200 OKErfolgDie Daten wurden erfolgreich gefunden und als JSON zurückgegeben.
400 Bad RequestUngültige AnfrageFalsche Parameter übergeben (z.B. ungültiges Datumsformat).
404 Not FoundNicht gefundenStadt, Benutzer oder URL-Endpunkt existiert nicht.
500 Internal Server ErrorServerfehlerUnerwarteter Fehler oder Absturz auf dem Zielserver.
+
+ + +
+

3. JSON-Parsing & Defensive Fehlerbehandlung

+ +

+ Im Web empfangen wir Daten fast immer als rohen Textstring. Mit json.loads() (load string) parsen wir ihn in Python: +

+ +
+
+ Sicheres Parsen verschachtelter JSON-Antworten +
+
import json
+
+roher_json_text = '''
+{
+    "name": "Hamburg",
+    "main": {"temp": 17.5, "humidity": 82},
+    "weather": [{"description": "Leichter Nieselregen"}]
+}
+'''
+
+try:
+    daten = json.loads(roher_json_text)
+    
+    # Defensiver Zugriff auf verschachtelte Werte:
+    stadt = daten["name"]
+    temperatur = float(daten["main"]["temp"])
+    beschreibung = daten["weather"][0]["description"]
+    
+except json.JSONDecodeError as e:
+    print(f"Kaputter JSON-String: {e}")
+except (KeyError, IndexError) as e:
+    print(f"Unerwartete Datenstruktur / Fehlendes Feld: {e}")
+
+
+ + +
+

4. 🎯 Deine Aufgaben in aufgabe.py

+ +

Öffne die Datei aufgabe.py und vervollständige die Web-API-Tools:

+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ +

💡 Lösungshinweise (Aufklappen bei Bedarf)

+ +
+ 🔍 Tipp zu parse_wetter_antwort +
+
+
try:
+    data = json.loads(json_text)
+    return {
+        "stadt": data["name"],
+        "temperatur": float(data["main"]["temp"]),
+        "luftfeuchtigkeit": int(data["main"]["humidity"]),
+        "wetterlage": data["weather"][0]["description"]
+    }
+except Exception as e:
+    raise ValueError(f"Ungültige Wetterdaten: {e}")
+
+
+
+ +
+ 🔍 Tipp zum Ticker-String +
+
+
items = [f"[{n['ressort']}] {n['titel']}" for n in nachrichten]
+return " +++ ".join(items) + " +++"
+
+
+
+
+ + +
+

5. 🧪 Lösung überprüfen

+

Führe im Terminal im Ordner 22_web_apis_und_json_feeds folgenden Befehl aus:

+
+
+ Terminal +
+
python3 test_aufgabe.py
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + diff --git a/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/musterloesung.py b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/musterloesung.py new file mode 100644 index 0000000..b923500 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/musterloesung.py @@ -0,0 +1,166 @@ +""" +Kapitel 22: Web-APIs, REST & JSON-Feeds 🌐📡 +============================================ +Lehrpfad 4: Professional Data Engineering + +Musterlösung für Web-APIs, JSON-Feeds und defensive Fehlerbehandlung. +""" + +import json +from typing import List, Dict, Any, Tuple, Optional, Union + + +def parse_wetter_antwort(json_text: str) -> Dict[str, Any]: + """ + Parst den JSON-String einer Wetter-API und extrahiert Stadt, Temperatur, + Luftfeuchtigkeit und Wetterlage. + """ + try: + data = json.loads(json_text) + stadt = data["name"] + temperatur = float(data["main"]["temp"]) + luftfeuchtigkeit = int(data["main"]["humidity"]) + + weather_list = data["weather"] + if not weather_list: + wetterlage = "Unbekannt" + else: + wetterlage = weather_list[0].get("description") or weather_list[0].get("main", "Unbekannt") + + return { + "stadt": stadt, + "temperatur": temperatur, + "luftfeuchtigkeit": luftfeuchtigkeit, + "wetterlage": str(wetterlage), + } + except (json.JSONDecodeError, KeyError, IndexError, TypeError, ValueError) as e: + raise ValueError(f"Ungültige Wetterdaten: {e}") + + +def filtriere_aktien_kurse(json_feed_text: str, min_kurs: float) -> List[Dict[str, Any]]: + """ + Filtert alle Aktien mit Kurs >= min_kurs heraus. + """ + try: + data = json.loads(json_feed_text) + if isinstance(data, dict) and "aktien" in data: + aktien_liste = data["aktien"] + elif isinstance(data, dict) and "stocks" in data: + aktien_liste = data["stocks"] + elif isinstance(data, list): + aktien_liste = data + else: + return [] + + ergebnis = [] + for item in aktien_liste: + if isinstance(item, dict) and "kurs" in item: + if float(item["kurs"]) >= float(min_kurs): + ergebnis.append(item) + elif isinstance(item, dict) and "price" in item: + if float(item["price"]) >= float(min_kurs): + ergebnis.append(item) + return ergebnis + except (json.JSONDecodeError, TypeError, ValueError): + return [] + + +def formatiere_nachrichten_ticker(news_json_text: str) -> str: + """ + Formatiert Schlagzeilen zu einem Ticker-String. + """ + try: + data = json.loads(news_json_text) + if isinstance(data, dict): + nachrichten = data.get("nachrichten") or data.get("articles") or [] + elif isinstance(data, list): + nachrichten = data + else: + return "+++ Keine aktuellen Eilmeldungen +++" + + if not nachrichten: + return "+++ Keine aktuellen Eilmeldungen +++" + + eintraege = [] + for item in nachrichten: + if isinstance(item, dict): + titel = item.get("titel") or item.get("title") or "Ohne Titel" + ressort = item.get("ressort") or item.get("category") or "Allgemein" + eintraege.append(f"[{ressort}] {titel}") + + if not eintraege: + return "+++ Keine aktuellen Eilmeldungen +++" + + return " +++ ".join(eintraege) + " +++" + except (json.JSONDecodeError, TypeError): + return "+++ Fehler beim Laden des Nachrichten-Feeds +++" + + +def validiere_api_antwort(status_code: int, payload_json_text: str) -> Tuple[bool, Union[Dict[str, Any], str]]: + """ + Prüft Statuscode und JSON-Payload einer API-Antwort. + """ + if status_code == 200: + try: + data = json.loads(payload_json_text) + return (True, data) + except json.JSONDecodeError: + return (False, "Ungültiges JSON-Format") + else: + # Fehlerstatuscode + error_detail = None + try: + err_data = json.loads(payload_json_text) + if isinstance(err_data, dict): + error_detail = err_data.get("error") or err_data.get("message") or err_data.get("detail") + except Exception: + pass + + if error_detail: + return (False, f"HTTP Fehler {status_code}: {error_detail}") + return (False, f"HTTP Fehler {status_code}") + + +class WetterApiParser: + """ + Objektorientierter Wetter-Parser. + """ + + def __init__(self, raw_json: str = ""): + self.raw_json: str = raw_json + self.daten: Optional[Dict[str, Any]] = None + if raw_json: + self.lade_daten(raw_json) + + def lade_daten(self, raw_json: str) -> bool: + """Lädt und validiert Wetter-JSON.""" + self.raw_json = raw_json + try: + self.daten = parse_wetter_antwort(raw_json) + return True + except ValueError: + self.daten = None + return False + + def get_temperatur(self) -> Optional[float]: + """Liefert die Temperatur oder None.""" + if self.daten is not None: + return self.daten.get("temperatur") + return None + + def get_stadt(self) -> Optional[str]: + """Liefert die Stadt oder None.""" + if self.daten is not None: + return self.daten.get("stadt") + return None + + def ist_frostig(self) -> bool: + """Gibt True zurück wenn Temperatur < 0.0 °C.""" + temp = self.get_temperatur() + if temp is not None: + return temp < 0.0 + return False + + def ist_gueltig(self) -> bool: + """Prüft ob valide Daten vorliegen.""" + return self.daten is not None diff --git a/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/test_aufgabe.py b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/test_aufgabe.py new file mode 100644 index 0000000..1624522 --- /dev/null +++ b/lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds/test_aufgabe.py @@ -0,0 +1,180 @@ +import unittest +import sys +import json +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Python-Pfad liegt +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel22(unittest.TestCase): + """ + Umfassende Offline-Testsuite für Modul 22 (REST-APIs, JSON-Feeds & Fehlerbehandlung). + Nutzt realistische Mock-JSON-Datensätze (100% Pyodide- und Offline-kompatibel). + """ + + def setUp(self): + self.parse_wetter_antwort = getattr(aufgabe, "parse_wetter_antwort", None) + self.filtriere_aktien_kurse = getattr(aufgabe, "filtriere_aktien_kurse", None) + self.formatiere_nachrichten_ticker = getattr(aufgabe, "formatiere_nachrichten_ticker", None) + self.validiere_api_antwort = getattr(aufgabe, "validiere_api_antwort", None) + self.WetterApiParser = getattr(aufgabe, "WetterApiParser", None) + + self.assertIsNotNone(self.parse_wetter_antwort, "Funktion 'parse_wetter_antwort' fehlt!") + self.assertIsNotNone(self.filtriere_aktien_kurse, "Funktion 'filtriere_aktien_kurse' fehlt!") + self.assertIsNotNone(self.formatiere_nachrichten_ticker, "Funktion 'formatiere_nachrichten_ticker' fehlt!") + self.assertIsNotNone(self.validiere_api_antwort, "Funktion 'validiere_api_antwort' fehlt!") + self.assertIsNotNone(self.WetterApiParser, "Klasse 'WetterApiParser' fehlt!") + + def test_01_parse_wetter_antwort_erfolg(self): + """Prüft die Extraktion von Stadt, Temperatur, Luftfeuchtigkeit und Wetterlage.""" + mock_payload = json.dumps({ + "name": "Berlin", + "coord": {"lon": 13.41, "lat": 52.52}, + "main": { + "temp": 19.4, + "feels_like": 18.9, + "humidity": 68 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "Mäßiger Regen", + "icon": "10d" + } + ] + }) + + ergebnis = self.parse_wetter_antwort(mock_payload) + self.assertIsInstance(ergebnis, dict, "parse_wetter_antwort muss ein Dictionary zurückgeben!") + self.assertEqual(ergebnis["stadt"], "Berlin") + self.assertAlmostEqual(ergebnis["temperatur"], 19.4, places=1) + self.assertEqual(ergebnis["luftfeuchtigkeit"], 68) + self.assertEqual(ergebnis["wetterlage"], "Mäßiger Regen") + + def test_02_parse_wetter_antwort_fehlerbehandlung(self): + """Prüft, dass bei ungültigem JSON oder fehlenden Feldern ein ValueError ausgelöst wird.""" + # 1. Korrupter JSON-String + with self.assertRaises(ValueError, msg="Ungültiger JSON-String muss einen ValueError werfen!"): + self.parse_wetter_antwort("{ungueltiges json: 42") + + # 2. Fehlendes 'main' Objekt + unvollstaendig = json.dumps({"name": "Hamburg", "weather": [{"description": "Sonne"}]}) + with self.assertRaises(ValueError, msg="Fehlende Pflichtfelder müssen einen ValueError werfen!"): + self.parse_wetter_antwort(unvollstaendig) + + def test_03_filtriere_aktien_kurse(self): + """Prüft das Filtern von Aktienkursen über dem Schwellenwert.""" + feed = json.dumps({ + "timestamp": "2026-08-29T12:00:00Z", + "aktien": [ + {"symbol": "NVDA", "kurs": 125.40, "waehrung": "USD"}, + {"symbol": "AAPL", "kurs": 220.10, "waehrung": "USD"}, + {"symbol": "PENNY", "kurs": 1.20, "waehrung": "EUR"}, + {"symbol": "MSFT", "kurs": 445.00, "waehrung": "USD"} + ] + }) + + gefiltert = self.filtriere_aktien_kurse(feed, 200.0) + self.assertIsInstance(gefiltert, list) + self.assertEqual(len(gefiltert), 2, "Es sollten genau 2 Aktien >= 200.0 $ gefunden werden!") + + symbole = [a["symbol"] for a in gefiltert] + self.assertIn("AAPL", symbole) + self.assertIn("MSFT", symbole) + self.assertNotIn("NVDA", symbole) + self.assertNotIn("PENNY", symbole) + + # Ungültiges JSON liefert leere Liste + self.assertEqual(self.filtriere_aktien_kurse("defektes json", 100.0), []) + + def test_04_formatiere_nachrichten_ticker(self): + """Prüft die Formatierung der Schlagzeilen als durchgehender Ticker.""" + feed = json.dumps({ + "nachrichten": [ + {"titel": "Python 3.13 veröffentlicht", "ressort": "Tech"}, + {"titel": "DAX schließt im Plus", "ressort": "Wirtschaft"} + ] + }) + + ticker = self.formatiere_nachrichten_ticker(feed) + self.assertIsInstance(ticker, str) + self.assertIn("[Tech] Python 3.13 veröffentlicht", ticker) + self.assertIn("[Wirtschaft] DAX schließt im Plus", ticker) + self.assertTrue(ticker.endswith("+++")) + + # Leere Liste + leerer_feed = json.dumps({"nachrichten": []}) + self.assertEqual(self.formatiere_nachrichten_ticker(leerer_feed), "+++ Keine aktuellen Eilmeldungen +++") + + # Defektes JSON + self.assertEqual(self.formatiere_nachrichten_ticker("###"), "+++ Fehler beim Laden des Nachrichten-Feeds +++") + + def test_05_validiere_api_antwort_200_ok(self): + """Prüft erfolgreiche API-Antworten (Status 200).""" + payload = json.dumps({"status": "success", "user_id": 42, "role": "admin"}) + status, daten = self.validiere_api_antwort(200, payload) + + self.assertTrue(status, "Statuscode 200 mit gültigem JSON muss True liefern!") + self.assertIsInstance(daten, dict) + self.assertEqual(daten["user_id"], 42) + + def test_06_validiere_api_antwort_200_bad_json(self): + """Prüft Status 200 mit defektem JSON Payload (z.B. Verbindungsabbruch).""" + status, msg = self.validiere_api_antwort(200, "502 Bad Gateway") + self.assertFalse(status) + self.assertEqual(msg, "Ungültiges JSON-Format") + + def test_07_validiere_api_antwort_fehler_status(self): + """Prüft HTTP-Fehlercodes (404, 500) und das Extrahieren von Fehlermeldungen.""" + # 404 mit Fehler-JSON + payload_404 = json.dumps({"error": "Benutzerprofil nicht gefunden", "code": 404}) + status_404, msg_404 = self.validiere_api_antwort(404, payload_404) + self.assertFalse(status_404) + self.assertIn("404", msg_404) + self.assertIn("Benutzerprofil nicht gefunden", msg_404) + + # 500 ohne JSON + status_500, msg_500 = self.validiere_api_antwort(500, "Internal Server Error") + self.assertFalse(status_500) + self.assertIn("500", msg_500) + + def test_08_wetter_api_parser_klasse(self): + """Prüft die objektorientierte Klasse WetterApiParser.""" + gueltiges_json = json.dumps({ + "name": "Innsbruck", + "main": {"temp": -3.5, "humidity": 80}, + "weather": [{"main": "Snow", "description": "Schneefall"}] + }) + + parser = self.WetterApiParser(gueltiges_json) + self.assertTrue(parser.ist_gueltig()) + self.assertEqual(parser.get_stadt(), "Innsbruck") + self.assertAlmostEqual(parser.get_temperatur(), -3.5, places=1) + self.assertTrue(parser.ist_frostig(), "-3.5 °C muss als frostig erkannt werden!") + + # Ungültige Daten laden + parser.lade_daten("kaputtes json") + self.assertFalse(parser.ist_gueltig()) + self.assertIsNone(parser.get_temperatur()) + self.assertIsNone(parser.get_stadt()) + self.assertFalse(parser.ist_frostig()) + + +if __name__ == "__main__": + res = unittest.main(verbosity=2, exit=False) + try: + root_dir = Path(__file__).parent.parent.parent.resolve() + sys.path.insert(0, str(root_dir)) + from gamification import report_single_chapter_result + rel_pfad = f"{Path(__file__).parent.parent.name}/{Path(__file__).parent.name}" + report_single_chapter_result(rel_pfad, res.result.wasSuccessful(), res.result.testsRun) + except Exception: + pass + sys.exit(0 if res.result.wasSuccessful() else 1) diff --git a/test_all.py b/test_all.py index e12b910..67e5f82 100644 --- a/test_all.py +++ b/test_all.py @@ -45,6 +45,14 @@ KAPITEL = [ ("Kapitel 14: Desktop-GUIs mit Tkinter", "lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter"), ("Kapitel 15: Parameter & Eigene Container", "lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container"), ("Kapitel 16: Master-Abschlussprojekt", "lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt"), + + # LEHRPFAD 4: PROFESSIONAL DATA ENGINEERING + ("Kapitel 17: Reguläre Ausdrücke (re)", "lehrpfad_4_professional_data_engineering/17_regulaere_ausdruecke_re"), + ("Kapitel 18: Funktionale Programmierung", "lehrpfad_4_professional_data_engineering/18_funktionale_programmierung_lambda"), + ("Kapitel 19: Generatoren & itertools", "lehrpfad_4_professional_data_engineering/19_generatoren_und_itertools"), + ("Kapitel 20: Dataclasses & Type Hints", "lehrpfad_4_professional_data_engineering/20_dataclasses_und_typehints"), + ("Kapitel 21: Datenbanken & SQLite", "lehrpfad_4_professional_data_engineering/21_datenbanken_und_sqlite"), + ("Kapitel 22: Web-APIs & JSON-Feeds", "lehrpfad_4_professional_data_engineering/22_web_apis_und_json_feeds"), ] def main(): diff --git a/workspace.html b/workspace.html index e9863cc..0e1f266 100644 --- a/workspace.html +++ b/workspace.html @@ -361,7 +361,13 @@ { track: "lehrpfad_3_fortgeschrittenes_oop", folder: "13_persistenz_json_und_csv", title: "Adv 13. Persistenz" }, { track: "lehrpfad_3_fortgeschrittenes_oop", folder: "14_gui_mit_tkinter", title: "Adv 14. Tkinter" }, { track: "lehrpfad_3_fortgeschrittenes_oop", folder: "15_parameter_und_container", title: "Adv 15. Container" }, - { track: "lehrpfad_3_fortgeschrittenes_oop", folder: "16_master_abschlussprojekt", title: "Master 16. Abschlussprojekt" } + { track: "lehrpfad_3_fortgeschrittenes_oop", folder: "16_master_abschlussprojekt", title: "Master 16. Abschlussprojekt" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "17_regulaere_ausdruecke_re", title: "Pro 17. Regex & Text Mining" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "18_funktionale_programmierung_lambda", title: "Pro 18. Functional & Lambda" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "19_generatoren_und_itertools", title: "Pro 19. Generatoren & Itertools" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "20_dataclasses_und_typehints", title: "Pro 20. Dataclasses & Decorators" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "21_datenbanken_und_sqlite", title: "Pro 21. SQLite & Relationale DBs" }, + { track: "lehrpfad_4_professional_data_engineering", folder: "22_web_apis_und_json_feeds", title: "Pro 22. REST APIs & JSON Feeds" } ]; function switchTab(tabId) {