Compare commits

...

51 Commits

Author SHA1 Message Date
6b28a6ab1d feat: pricing function for test tutorial test: test cases for pricing 2026-03-26 14:47:33 +01:00
ed538a8810 merge 2026-03-26 11:28:19 +01:00
1a3284cb55 refractor: changed comment to describe current state 2026-03-24 22:38:12 +01:00
1a5073c2a7 feat: function correctly encodes arabic number to chinese 2026-03-24 22:35:58 +01:00
ada5130c9b test: added test case for Chinese Numeral Encoder kata 2026-03-24 22:21:23 +01:00
c9bcc399d6 feat: function now creates blueprint for integer part 2026-03-24 21:25:05 +01:00
96aa72d123 refractor: start of new concept. Will now also incorporate "Chinese 0 rule" and fractions 2026-03-24 20:20:34 +01:00
25bc41726e chore: added comments 2026-03-24 18:07:36 +01:00
1c2f806960 feat: concept to arrange number by chinese numeral logic 2026-03-23 22:21:11 +01:00
6fafcfac08 refractor: removed unncessary comment 2026-03-23 21:57:19 +01:00
a5e72e9402 feat: will now return numerals from 0 to 19 correctly 2026-03-23 21:43:17 +01:00
81b25abc2b build: removed preloaded from requirements. Not needed 2026-03-23 21:22:52 +01:00
5ab3c0535a test: added test cases for kata Chinese Numeral Encoder 2026-03-23 20:44:32 +01:00
030979e4a0 build: added preloaded to requirements 2026-03-23 20:35:27 +01:00
f90bc95256 chore, added description to README for kata Chinese Numeral Encoder 2026-03-23 20:30:45 +01:00
1d684b4d65 feat: order, logging and try tutorial 2026-03-23 18:04:15 +01:00
060362ec09 feat: json exercise 2026-03-19 13:28:20 +01:00
e28a2bdc67 feat: pathlib exercise 2026-03-19 11:58:22 +01:00
a455b46b72 refractor: added comment and removed infinite loop guardrail 2026-03-17 18:07:52 +01:00
9ff7d9161e feat: kata snail now has function, which rearanges list based on kata rules. Pre test commit, may still be incorrect 2026-03-17 18:03:24 +01:00
11f8bf5394 feat: list created, where endresult will be saved 2026-03-16 18:19:01 +01:00
f722614590 feat: initial commit for kata 'Snail'. test: initial commit for kata 'Snail' 2026-03-16 18:09:21 +01:00
fdcde7a855 chore: Kata description for Snail added to README 2026-03-16 17:50:23 +01:00
e867f69ad9 build: changed name of test/codewars to test/test_codewars 2026-03-13 17:49:29 +01:00
87eea6433e Merge branch 'tutorial' 2026-03-13 17:44:57 +01:00
60cf2dd6c6 feat: module and import exercises 2026-03-13 17:44:37 +01:00
03427a784f Merge pull request 'task2' (#1) from task2 into main
Reviewed-on: #1

Nein, von der Struktur sieht das sehr schön aus. Du hast sogar tests (z.B. für piracy-OOP) eingefügt, das ist natürlich sehr vorbildlich. Weil Du dort den Order tests/codewars/test___ nennst, bin ich nicht ganz sicher, ob dieser in jedem falle automatisch als Testordner erkannt wird (wegen dem codewars). Wenn Du viele Tests in einem Order tests/ hast und alle Files mit test_ beginnen, kannst Du alle zusammen automatisch ausführen mit pytest in der Konsole (oder auch einzeln unter Angabe des exakten Files).
2026-03-13 10:24:15 +01:00
7d1e778902 Merge pull request 'task3' (#2) from task3 into main
Reviewed-on: #2

Wow, alle Beispiele gelöst, sehr schön. Wenn Du konkrete Feedbacks willst, müsstest Du diese einfach stellen.
Was mir gefällt ist die strukturierte Beschrifftung der commits (feat etc.) und dass Du alle Aufgaben modular commitest.
2026-03-13 10:11:17 +01:00
ce9a4029b7 refractor: removed print statement, that was ment for development 2026-03-11 23:57:28 +01:00
9df3b783fb feat: kata finished. Now correctly finds next bigger number. test: added test cases 2026-03-11 23:41:59 +01:00
fbe9f4bf02 function will now swamp pivot with lowest number right of pivot 2026-03-11 22:22:40 +01:00
c16bdad9b9 feat: function now finds pivot of number. Returns -1 if there is no pivot 2026-03-11 22:02:34 +01:00
5797eb8ef9 test: added a large number test case for kata 'Next bigger number with the same digits' 2026-03-11 21:44:56 +01:00
01a6ff1ead feat: kata next biggest number. Works but struggles with big numbers. test: test cases for kata 'Next bigger number with the same digits' 2026-03-11 17:45:04 +01:00
bc00bcf5f3 chore: added context for kata 'Next bigger number with the same digits' 2026-03-11 17:19:06 +01:00
d010de0fe5 test: test cases for kata 'Who has the most money' 2026-03-10 16:38:11 +01:00
c00cbd030b feat: kata 'Who has the most money' compleated. 2026-03-10 16:36:56 +01:00
a681ae4f2b chore: README updated for kata 'Who has the most money' 2026-03-10 15:24:22 +01:00
90ba3e1e60 chore: README updated for kata 'Who has the most money' 2026-03-10 15:24:00 +01:00
e521f037a4 refractor: removed unnecessary comment 2026-03-09 21:20:36 +01:00
00a42ea0fd fix: fixed function page_item_count. from calculating: constant * muliplicator * 2. to: constant * multiplicator + constant. first calculation returned wrong amount of items to return 2026-03-09 21:19:09 +01:00
fa8b2b21f9 fix: forgot to return len() of list in function page_item_count. Insted list itself was returned 2026-03-09 20:20:52 +01:00
7e43f0d42d feat: function page_count and page_item_count finished 2026-03-09 20:14:59 +01:00
fbf13b3f3f feat: function item_count() finished, working on page_count() 2026-03-09 18:36:38 +01:00
17de0e0f92 test: added test cases for kata 'PaginationHelper' 2026-03-09 18:20:09 +01:00
27c5087006 chore: README updated for kata content 'PaginationHelper' 2026-03-09 18:03:58 +01:00
99e6d45790 Merge branch 'task2' of https://gitea.fhgr.ch/zimmersandro/ppe2 into task2 2026-03-06 18:36:52 +01:00
35f4b12c34 feat: finished kata building_blocks, thinkful_quarks and thinkful_vectors. test: finished test cases for katas 2026-03-06 18:32:13 +01:00
95d79c84a8 feat: added inheritace and comparisond methods 2026-03-06 16:38:28 +01:00
8f3cd117ef feat: version manager kata done. test: testcase for version manager kata started 2026-03-06 10:22:31 +01:00
ec105d44be feat: kata version manager __init__ configured and methods created 2026-03-05 23:56:33 +01:00
56 changed files with 18479 additions and 17 deletions

View File

@ -15,9 +15,9 @@ Repository for CDS-2020 Programming and Promt Engineering II
|build|Build-System, Dependencies|build: update requirements.txt|
# Codewars
|Title|Source (src/codewars/)|Test (test/codewars/)|URL|
|Title|Source (src/codewars/)|Test (test/test_codewars/)|URL|
|-|-|-|-|
|Find the force of gravity between two objects|kata_force_of_gravity.py|test_force_of_gravity.py|[5b609ebc8f47bd595e000627](https://www.codewars.com/kata/5b609ebc8f47bd595e000627/)|
|Find the force of gravity between two objects|kata_force_of_gravity.py|test_force_of_gravity.py|[5b609ebc8f47bd595e000627](https://www.codewars.com/kata/5b609ebc8f47bd595e000627)|
|The Lamp: Revisited|kata_the_lamp.py|test_the_lamp.py|[570e6e32de4dc8a8340016dd](https://www.codewars.com/kata/570e6e32de4dc8a8340016dd)|
|OOP: Object Oriented Piracy|kata_object_oriented_piracy.py|test_object_oriented_piracy.py|[54fe05c4762e2e3047000add](https://www.codewars.com/kata/54fe05c4762e2e3047000add)|
|Vigenère Cipher Helper|kata_vigenere_cipher_helper.py|test_vigenere_cipher_helper.py|[52d1bd3694d26f8d6e0000d3](https://www.codewars.com/kata/52d1bd3694d26f8d6e0000d3)|
@ -26,3 +26,8 @@ Repository for CDS-2020 Programming and Promt Engineering II
|Thinkful - Object Drills: Quarks|kata_thinkful_quarks.py|test_thinkful_quarks.py|[5882b052bdeafec15e0000e6](https://www.codewars.com/kata/5882b052bdeafec15e0000e6)|
|Thinkful - Object Drills: Vectors|kata_thinkful_vectors.py|test_thinkful_vectors.py|[587f1e1f39d444cee6000ad4](https://www.codewars.com/kata/587f1e1f39d444cee6000ad4)|
|Building blocks|kata_building_blocks.py|test_building_blocks.py|[55b75fcf67e558d3750000a3](https://www.codewars.com/kata/55b75fcf67e558d3750000a3)|
|PaginationHelper|kata_pagination_helper.py|test_pagination_helper.py|[515bb423de843ea99400000a](https://www.codewars.com/kata/515bb423de843ea99400000a)|
|Who has the most money?|kata_who_the_most_money.py|test_who_the_most_money.py|[528d36d7cc451cd7e4000339](https://www.codewars.com/kata/528d36d7cc451cd7e4000339)|
|Next bigger number with the same digits|kata_next_bigger_number_same_digits.py|test_next_bigger_number_same_digits.py|[55983863da40caa2c900004e](https://www.codewars.com/kata/55983863da40caa2c900004e)|
|Snail|kata_snail.py|test_snail.py|[521c2db8ddc89b9b7a0000c1](https://www.codewars.com/kata/521c2db8ddc89b9b7a0000c1)|
|Chinese Numeral Encoder|kata_chinese_numeral_encoder.py|test_chinese_numeral_encoder.py|[52608f5345d4a19bed000b31](https://www.codewars.com/kata/52608f5345d4a19bed000b31)|

View File

@ -0,0 +1,24 @@
class Block:
def __init__(self, dimensions: list):
self.width = dimensions[0]
self.length = dimensions[1]
self.height = dimensions[2]
def get_width(self):
return self.width
def get_length(self):
return self.length
def get_height(self):
return self.height
def get_volume(self):
return self.width * self.length * self.height
def get_surface_area(self):
return (
2 * self.length * self.height
+ 2 * self.width * self.height
+ 2 * self.width * self.length
)

View File

@ -0,0 +1,109 @@
numerals = {
"-": "",
".": "",
0: "",
1: "",
2: "",
3: "",
4: "",
5: "",
6: "",
7: "",
8: "",
9: "",
10: "",
100: "",
1000: "",
10000: "",
}
# "Ten^x" dictonary
T = {1: 0, 2: 10, 3: 100, 4: 1000, 5: 10000}
def to_chinese_numeral(n):
# Check if negative, turn positive and remeber state
if n < 0:
n = abs(n)
is_negative = True
else:
is_negative = False
# return numerals[n] if n in numerals dictonary
if n % 100 != 0:
try:
if is_negative:
return "".join([numerals["-"], numerals[n]])
else:
return numerals[n]
except KeyError:
pass
# Split number in iteger list and fractional (None if not exists)
if type(n) is not int:
n = str(n).split(".")
integer = [int(x) for x in n[0]]
fractional = [int(x) for x in n[1]]
else:
integer = [int(x) for x in str(n)]
fractional = None
# Blueprint list will contain building instruction on how to encode the number: 90090 -> [9, 10000, 0, 9, 10]
blueprint = []
blueprint.append(integer[0]) # First digit won't be 0 it can just be appended
blueprint.append(T[len(integer)]) # Length of integer determines power of ten
integer.pop(0) # Pop first digit
# create rest of blueprint by looping rest of integer
for _ in range(0, len(integer)):
# 0 needs no power of ten
if integer[0] == 0:
blueprint.append(0)
else:
blueprint.append(integer[0])
blueprint.append(T[len(integer)])
integer.pop(0)
# Pop last digit in blueprint if it's a 0
for i in range(len(blueprint) - 1, 0, -1):
if blueprint[i] == 0:
blueprint.pop(i)
else:
break
# Remove grouped zeros
was_zero = False
index_to_remove = []
for j, i in enumerate(blueprint):
if i == 0 and not was_zero:
was_zero = True
elif i == 0 and was_zero:
index_to_remove.append(j)
elif i != 0 and was_zero:
was_zero = False
index_to_remove.sort(
reverse=True
) # Reverse indices to avoid index errors when applying pop()
[blueprint.pop(x) for x in index_to_remove]
# remove first item in blueprint if second item in blueprint is between 10 and 19. Skip step if index error
try:
if sum(blueprint[0:2]) == 11:
blueprint.pop(0)
except IndexError:
pass
# Add symbols - if n was negative and . if it was a fraction
if is_negative:
blueprint.insert(0, "-")
if fractional is not None:
blueprint.append(".")
[blueprint.append(x) for x in fractional]
# Apply chinese encoding to blueprint and fractional
encoding = [numerals[x] for x in blueprint]
return "".join(encoding)

View File

@ -0,0 +1,31 @@
def next_bigger(n):
# get number as digits in list
digits = [int(x) for x in list(str(n))]
# find index of pivot. return -1 if there is no pivot, meaning number is alreadig biggest
pivot = -1
for i in range(1, len(digits)):
if digits[-i] > digits[-i - 1]:
pivot = -i - 1
break
if pivot == -1:
return -1
# find the smallest digit to the right that is bigger than the pivot
right = digits[pivot + 1 :]
swap = right.index(min([x for x in right if x > digits[pivot]]))
# swap pivot with found digit
digits[pivot], digits[len(digits) - len(right) + swap] = (
right[swap],
digits[pivot],
)
# sort right side of new swapped pivot and replace it at the end
right = digits[pivot + 1 :]
right.sort()
digits[pivot + 1 :] = right
# return number
return int("".join([str(x) for x in digits]))

View File

@ -0,0 +1,46 @@
class PaginationHelper:
# The constructor takes in an array of items and an integer indicating
# how many items fit within a single page
def __init__(self, collection, items_per_page):
self.collection = collection
self.items_per_page = items_per_page
# returns the number of items within the entire collection
def item_count(self):
return len(self.collection)
# returns the number of pages
def page_count(self):
return int(len(self.collection) / self.items_per_page) + (
len(self.collection) % self.items_per_page > 0
)
# returns the number of items on the given page. page_index is zero based
# this method should return -1 for page_index values that are out of range
def page_item_count(self, page_index):
if page_index < 0 or self.page_count() - 1 < page_index:
return -1
if page_index == 0:
return len(self.collection[: self.items_per_page])
return len(
self.collection[
self.items_per_page * page_index : self.items_per_page * page_index
+ self.items_per_page
]
)
# determines what page an item at the given index is on. Zero based indexes.
# this method should return -1 for item_index values that are out of range
def page_index(self, item_index):
try:
self.collection[item_index]
if item_index < 0:
raise IndexError
except IndexError:
return -1
return item_index // self.items_per_page

View File

@ -0,0 +1,86 @@
def snail(snail_map):
# first list in array is always first. So add it to new list snail and pop it from snail_map
snail = [x for x in snail_map[0]]
snail_map.pop(0)
# transpone list and append first list to snail. Then pop firt item of snail_map. Loop till snail_map is empty
while len(snail_map) > 0:
snail_map = [list(reversed(x)) for x in snail_map]
snail_map = [list(row) for row in zip(*snail_map)]
[snail.append(x) for x in snail_map[0]]
snail_map.pop(0)
return snail
array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
array = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]
print(snail(array))
"""
Konzept:
Ziel: 1 2 3 6 9 8 7 4 5
1 2 3
4 5 6
7 8 9
1 2 3
4 5 6
7 8 9
6 9
5 8
4 7
1 2 3 6 9
5 8
4 7
8 7
5 4
1 2 3 6 9 8 7
5 4
4
5
1 2 3 6 9 8 7 4
5
1 2 3 6 9 8 7 4 5
Ziel: 1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
8 12 16
7 11 15
6 10 14
5 9 13
1 2 3 4 8 12 16
7 11 15
6 10 14
5 9 13
...
"""

View File

@ -0,0 +1,8 @@
class Quark(object):
def __init__(self, color, flavor):
self.color = color
self.flavor = flavor
self.baryon_number = 1 / 3
def interact(self, other) -> None:
self.color, other.color = other.color, self.color

View File

@ -0,0 +1,7 @@
class Vector(object):
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def add(self, other):
return Vector(self.x + other.x, self.y + other.y)

View File

@ -1,3 +1,47 @@
class VersionManager:
def __init__(self, version):
pass
def __init__(self, version=None):
if not version:
version = "0.0.1"
parts = version.split(".")[:3]
if not all(p.isdecimal() for p in parts):
raise ValueError("Error occured while parsing version!")
nums = [int(p) for p in parts]
while len(nums) < 3:
nums.append(0)
self.major_v, self.minor_v, self.patch_v = nums
self.history = []
def _save(self):
self.history.append((self.major_v, self.minor_v, self.patch_v))
def major(self):
self._save()
self.major_v += 1
self.minor_v = 0
self.patch_v = 0
return self
def minor(self):
self._save()
self.minor_v += 1
self.patch_v = 0
return self
def patch(self):
self._save()
self.patch_v += 1
return self
def rollback(self):
if not self.history:
raise Exception("Cannot rollback!")
self.major_v, self.minor_v, self.patch_v = self.history.pop()
return self
def release(self):
return f"{self.major_v}.{self.minor_v}.{self.patch_v}"

View File

@ -0,0 +1,15 @@
class Student:
def __init__(self, name, fives, tens, twenties):
self.name = name
self.fives = fives
self.tens = tens
self.twenties = twenties
def most_money(students):
student_names = [x.name for x in students]
student_money = [x.fives * 5 + x.tens * 10 + x.twenties * 20 for x in students]
if len(set(student_money)) == 1 and len(student_money) > 1:
return "all"
return max(list(zip(student_names, student_money)), key=lambda x: x[1])[0]

View File

@ -1,41 +1,88 @@
class Hund:
from abc import ABC, abstractmethod
class Hund(ABC):
anzahl_hunde = 0
def __init__(self, name: str, rasse: str, alter: int, gewicht: float):
def __init__(self, name: str, alter: int, gewicht: float, bellgeraeusch: str):
self.name = name
self.rasse = rasse
self.alter = alter
self.gewicht = gewicht
self.bellgeraeusch = bellgeraeusch
Hund.anzahl_hunde += 1
def __repr__(self):
return f"Hund(name={self.name!r}, rasse={self.rasse}, alter={self.alter}, gewicht={self.gewicht}"
def __repr__(self) -> str:
return f"Hund(name={self.name!r}, rasse={self.__class__.__name__}, alter={self.alter}, gewicht={self.gewicht}"
def __str__(self):
return f"{self.name} ist ein {self.alter}-jähriger {self.rasse}"
def __str__(self) -> str:
return f"{self.name} ist ein {self.alter}-jähriger {self.__class__.__name__}"
def bellen(self, n=1) -> int:
print(n * "Woof! ")
@abstractmethod
def bellen(self, n: int = 1) -> None:
print(" ".join([self.bellgeraeusch] * n))
def geburtstag(self):
def geburtstag(self) -> None:
self.alter += 1
print(
f"Alles Gute zum Geburtstag, {self.name}! Du bist jetzt {self.alter} Jahre alt."
)
def ist_welpe(self):
def ist_welpe(self) -> None:
if self.alter < 2:
print(f"{self.name} ist ein {self.alter}-jähriger Welpe")
else:
print(f"{self.name} ist ein {self.alter}-jähriger erwachsener Hund")
def __lt__(self, other): # less than: self < other
return self.alter < other.alter
hund1 = Hund(name="Bello", rasse="Pudel", alter=99, gewicht=357)
hund2 = Hund(name="Dewy", rasse="Labrador", alter=-6, gewicht=1)
def __le__(self, other): # less equal: self <= other
return self.alter <= other.alter
def __gt__(self, other): # greater than: self > other
return self.alter > other.alter
def __ge__(self, other): # greater equal: self >= other
return self.alter >= other.alter
def __eq__(self, other): # equal: self == other
return self.alter == other.alter
class Pudel(Hund):
def __init__(self, name, alter, gewicht):
super().__init__(name, alter, gewicht, "wau")
def bellen(self, n=1):
super().bellen(n)
class Labrador(Hund):
def __init__(self, name, alter, gewicht):
super().__init__(name, alter, gewicht, "wuff")
def bellen(self, n=1):
super().bellen(n)
class Bulldog(Hund):
def __init__(self, name, alter, gewicht):
super().__init__(name, alter, gewicht, "woff")
def bellen(self, n=1):
super().bellen(n)
hund1 = Labrador(name="Bello", alter=33, gewicht=27)
hund2 = Pudel(name="Dewy", alter=6, gewicht=1)
hund3 = Bulldog(name="Stone", alter=15, gewicht=1000)
print(repr(hund1))
print(hund2)
hund2.bellen(3)
hund1.bellen(2)
hund1.geburtstag()
hund2.ist_welpe()
hund1.ist_welpe()
hund3.bellen(4)
print(hund3 > hund2)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
import json
import logging
from pathlib import Path
logger = logging.getLogger(f"orders.{__name__}") # → "orders.load_files"
# ══════════════════════════════════════════════════════════════════════════════
# JSON-File einlesen
# ══════════════════════════════════════════════════════════════════════════════
def load_orders(path: str | Path) -> list[dict] | None:
"""
Liest eine JSON-Datei mit Bestellungen ein.
Behandelte Fehler
-----------------
UnicodeDecodeError Falsche Kodierung (z. B. Latin-1 statt UTF-8)
json.JSONDecodeError Ungültiges JSON (Syntaxfehler)
Rückgabe
--------
Liste der Bestellungen bei Erfolg, None bei Fehler.
"""
path = Path(path)
logger.info("Lese Datei: %s", path)
try:
with path.open("r", encoding="utf-8") as f:
loaded_file = json.load(f)
logger.info(f"{path} erfolgreich eingelesen")
return loaded_file
except FileNotFoundError:
logger.warning("Datei nicht gefunden: %s", path)
return None
except json.JSONDecodeError:
logger.warning(f"Konnte Datei {path} nicht decodieren")
return None
except UnicodeDecodeError as e:
logger.warning(f"Datei {path} scheint ein falsches Coding zu haben")
logger.debug(
f"UnicodeDecodeError-Details: "
f"encoding={e.encoding}, "
f"reason={e.reason}, "
f"start={e.start}, "
f"end={e.end}, "
f"bad_bytes={hex(e.object[e.start])}"
)
return None

View File

@ -0,0 +1,45 @@
"""
main.py Bestellungen einlesen und validieren
Demonstriert:
- Standard-Logging (logging-Modul) mit FileHandler + StreamHandler
- Sauberes Exception-Handling für UnicodeDecodeError & json.JSONDecodeError
- Eigene Exception-Klasse InvalidOrderError (erbt von ValueError)
"""
from pathlib import Path
from utils import setup_logger_extended
from load_files import load_orders
from validation import process_orders
def main() -> None:
logger = setup_logger_extended("orders")
files = [
"orders_1_valid.json",
"orders_5_non_existing_file.json",
"orders_2_parse_error.json",
"orders_3_encoding_error.json",
"orders_4_invalid_order.json",
]
for filename in files:
BASE_DIR = Path(__file__).parent
file_path = BASE_DIR / "data" / filename
logger.info("=" * 60)
logger.info("Verarbeite: %s", filename)
orders = load_orders(file_path)
if orders is not None:
process_orders(orders)
logger.info("=" * 60)
logger.info(f"Alle {len(files)} Dateien verarbeitet. Details siehe orders.log")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,225 @@
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Verarbeite: orders_1_valid.json
2026-03-23 17:54:26 | INFO | orders.load_files | Lese Datei: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_1_valid.json
2026-03-23 17:54:26 | INFO | orders.load_files | /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_1_valid.json erfolgreich eingelesen
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00001
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00002
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00003
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00004
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00005
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00006
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00007
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00008
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00009
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00010
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00011
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00012
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00013
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00014
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00015
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00016
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00017
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00018
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00019
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00020
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00021
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00022
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00023
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00024
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00025
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00026
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00027
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00028
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00029
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00030
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00031
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00032
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00033
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00034
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00035
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00036
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00037
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00038
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00039
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00040
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00041
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00042
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00043
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00044
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00045
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00046
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00047
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00048
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00049
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00050
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00051
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00052
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00053
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00054
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00055
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00056
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00057
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00058
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00059
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00060
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00061
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00062
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00063
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00064
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00065
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00066
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00067
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00068
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00069
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00070
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00071
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00072
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00073
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00074
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00075
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00076
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00077
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00078
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00079
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00080
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00081
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00082
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00083
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00084
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00085
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00086
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00087
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00088
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00089
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00090
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00091
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00092
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00093
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00094
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00095
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00096
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00097
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00098
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00099
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00100
2026-03-23 17:54:26 | INFO | orders.validation | Validierung abgeschlossen: 100 gültig, 0 ungültig.
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Verarbeite: orders_5_non_existing_file.json
2026-03-23 17:54:26 | INFO | orders.load_files | Lese Datei: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_5_non_existing_file.json
2026-03-23 17:54:26 | WARNING | orders.load_files | Datei nicht gefunden: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_5_non_existing_file.json
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Verarbeite: orders_2_parse_error.json
2026-03-23 17:54:26 | INFO | orders.load_files | Lese Datei: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_2_parse_error.json
2026-03-23 17:54:26 | WARNING | orders.load_files | Konnte Datei /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_2_parse_error.json nicht decodieren
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Verarbeite: orders_3_encoding_error.json
2026-03-23 17:54:26 | INFO | orders.load_files | Lese Datei: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_3_encoding_error.json
2026-03-23 17:54:26 | WARNING | orders.load_files | Datei /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_3_encoding_error.json scheint ein falsches Coding zu haben
2026-03-23 17:54:26 | DEBUG | orders.load_files | UnicodeDecodeError-Details: encoding=utf-8, reason=invalid start byte, start=118, end=119, bad_bytes=0xfc
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Verarbeite: orders_4_invalid_order.json
2026-03-23 17:54:26 | INFO | orders.load_files | Lese Datei: /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_4_invalid_order.json
2026-03-23 17:54:26 | INFO | orders.load_files | /Users/s/workspace/gittea.fhgr.ch/zimmersandro/ppe2/src/tutorial/files_and_path/order/data/orders_4_invalid_order.json erfolgreich eingelesen
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00001
2026-03-23 17:54:26 | WARNING | orders.validation | Ungültige Bestellung — order_id=ORD-00002, feld=qty, wert=-3: Negative Menge (-3) ist nicht erlaubt.
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00003
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00004
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00005
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00006
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00007
2026-03-23 17:54:26 | WARNING | orders.validation | Ungültige Bestellung — order_id=ORD-00008, feld=qty, wert=-4: Negative Menge (-4) ist nicht erlaubt.
2026-03-23 17:54:26 | WARNING | orders.validation | Ungültige Bestellung — order_id=ORD-00009, feld=total_chf, wert=-12.75: Negativer Gesamtbetrag (-12.75 CHF) ist nicht erlaubt.
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00010
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00011
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00012
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00013
2026-03-23 17:54:26 | WARNING | orders.validation | Ungültige Bestellung — order_id=ORD-00014, feld=qty, wert=-2: Negative Menge (-2) ist nicht erlaubt.
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00015
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00016
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00017
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00018
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00019
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00020
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00021
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00022
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00023
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00024
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00025
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00026
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00027
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00028
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00029
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00030
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00031
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00032
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00033
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00034
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00035
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00036
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00037
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00038
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00039
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00040
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00041
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00042
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00043
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00044
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00045
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00046
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00047
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00048
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00049
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00050
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00051
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00052
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00053
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00054
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00055
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00056
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00057
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00058
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00059
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00060
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00061
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00062
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00063
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00064
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00065
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00066
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00067
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00068
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00069
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00070
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00071
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00072
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00073
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00074
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00075
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00076
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00077
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00078
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00079
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00080
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00081
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00082
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00083
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00084
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00085
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00086
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00087
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00088
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00089
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00090
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00091
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00092
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00093
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00094
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00095
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00096
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00097
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00098
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00099
2026-03-23 17:54:26 | DEBUG | orders.validation | OK: ORD-00100
2026-03-23 17:54:26 | INFO | orders.validation | Validierung abgeschlossen: 96 gültig, 4 ungültig.
2026-03-23 17:54:26 | INFO | orders | ============================================================
2026-03-23 17:54:26 | INFO | orders | Alle 5 Dateien verarbeitet. Details siehe orders.log

View File

@ -0,0 +1,45 @@
import sys
import logging
# ══════════════════════════════════════════════════════════════════════════════
# Logging-Konfiguration
# ══════════════════════════════════════════════════════════════════════════════
LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s"
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
def setup_logger_extended(name: str, log_file: str = "orders.log") -> logging.Logger:
"""
Erstellt und konfiguriert einen Logger mit zwei Handlern:
- StreamHandler Ausgabe auf die Konsole (ab INFO)
- FileHandler Ausgabe in eine Log-Datei (ab DEBUG)
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG) # Root-Level: alles durchlassen
# Konsole: INFO und höher
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
# Log-File: DEBUG und höher (detaillierter)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
logger.addHandler(stream_handler)
logger.addHandler(file_handler)
return logger
def setup_logger(name: str = None, log_file: str = "orders.log") -> logging.Logger:
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger(__name__)
return logger

View File

@ -0,0 +1,115 @@
import logging
logger = logging.getLogger(f"orders.{__name__}") # → "orders.validation"
# ══════════════════════════════════════════════════════════════════════════════
# Eigene Exception-Klasse
# ══════════════════════════════════════════════════════════════════════════════
class InvalidOrderError(ValueError):
"""
Wird ausgelöst, wenn eine Bestellung ungültige Geschäftsdaten enthält.
Erbt von ValueError, weil es sich um einen inhaltlichen Wertfehler handelt
(kein technisches I/O-Problem).
Attribute
---------
order_id : str
Die ID der fehlerhaften Bestellung.
field : str
Der Name des ungültigen Feldes (z. B. "qty").
value : object
Der tatsächliche (ungültige) Wert.
message : str
Lesbare Fehlerbeschreibung (auch als str(e) verfügbar).
"""
def __init__(self, order_id: str, field: str, value: object, message: str):
self.order_id = order_id
self.field = field
self.value = value
self.message = message
super().__init__(message)
def __str__(self) -> str:
return (
f"InvalidOrderError | order_id={self.order_id!r} "
f"| field={self.field!r} | value={self.value!r} "
f"| {self.message}"
)
# ══════════════════════════════════════════════════════════════════════════════
# Geschäftslogik: Validierung einer einzelnen Bestellung
# ══════════════════════════════════════════════════════════════════════════════
def validate_order(order: dict) -> None:
"""
Prüft eine einzelne Bestellung auf Plausibilität.
Wirft InvalidOrderError, sobald ein Regelverstoß entdeckt wird.
Regeln (erweiterbar):
- qty darf nicht negativ sein
- total_chf darf nicht negativ sein
- order_id und status müssen vorhanden sein
"""
order_id = order.get("order_id", "<unbekannt>")
for item in order.get("items", []):
qty = item.get("qty")
if qty is not None and qty < 0:
raise InvalidOrderError(
order_id=order_id,
field="qty",
value=qty,
message=f"Negative Menge ({qty}) ist nicht erlaubt.",
)
total = order.get("total_chf")
if total is not None and total < 0:
raise InvalidOrderError(
order_id=order_id,
field="total_chf",
value=total,
message=f"Negativer Gesamtbetrag ({total} CHF) ist nicht erlaubt.",
)
if not order.get("order_id"):
raise InvalidOrderError(
order_id="<unbekannt>",
field="order_id",
value=order.get("order_id"),
message="Pflichtfeld 'order_id' fehlt oder ist leer.",
)
# ══════════════════════════════════════════════════════════════════════════════
# Validierungs-Durchlauf über alle Bestellungen
# ══════════════════════════════════════════════════════════════════════════════
def process_orders(orders: list[dict]) -> None:
"""
Iteriert über alle Bestellungen und validiert jede einzelne.
Ungültige Bestellungen werden geloggt und übersprungen (kein Abbruch).
"""
valid_count = 0
invalid_count = 0
for order in orders.get("orders"):
try:
validate_order(order)
valid_count += 1
logger.debug("OK: %s", order.get("order_id"))
except InvalidOrderError as e:
invalid_count += 1
logger.warning(
f"Ungültige Bestellung — order_id={e.order_id}, feld={e.field}, wert={e.value}: {e.message}"
)
logger.info(
f"Validierung abgeschlossen: {valid_count} gültig, {invalid_count} ungültig."
)

View File

@ -0,0 +1,6 @@
from pathlib import Path
base_dir = Path("data")
file_path = base_dir / "raw" / "processed.json"
file_path.mkdir(parents=True, exist_ok=True)

View File

@ -0,0 +1,33 @@
# Ziel dieses Skripts:
#
# 1. Das Produkt von 6 * 7 berechnen → Erwartet: 42.0
# 2. Die Kreisfläche für Radius 5 berechnen → Erwartet: ~78.54
# 3. Den genauen Wert von PI ausgeben → Erwartet: 3.141592653589793
#
# ----------------------------------------------------------------------
# AUFGABE: Das Skript liefert falsche Ergebnisse. Finde den Fehler und
# korrigiere die Import-Anweisungen ohne den restlichen Code
# zu verändern!
# ----------------------------------------------------------------------
# from src.mathematik import berechne, PI # (1) Import aus mathematik
# from src.geometrie import berechne, PI # (2) Import aus geometrie
import src.geometrie as geo
import src.mathematik as mathe
def main():
# --- Aufgabe 1: Produkt berechnen (nutzt mathematik.berechne) ---
produkt = mathe.berechne(6, 7) # Erwartet: 42.0 — bekommt aber einen Fehler!
print(f"6 × 7 = {produkt}")
# --- Aufgabe 2: Kreisfläche (nutzt geometrie.berechne) ---
flaeche = geo.berechne(5) # Erwartet: ~78.54
print(f"Kreisfläche (r=5): {flaeche:.4f}")
# --- Aufgabe 3: PI-Wert ---
print(f"PI = {mathe.PI}") # Erwartet: 3.141592653589793
if __name__ == "__main__":
main()

View File

@ -0,0 +1,14 @@
# geometrie.py
# Modul für geometrische Berechnungen (Kreisflächen, Umfang usw.)
PI = 3.14 # Vereinfachter Wert (nur 2 Nachkommastellen!)
def berechne(radius: float) -> float:
"""Berechnet die Fläche eines Kreises: A = PI * r²"""
return PI * radius**2
def umfang(radius: float) -> float:
"""Berechnet den Umfang eines Kreises: U = 2 * PI * r"""
return 2 * PI * radius

View File

@ -0,0 +1,14 @@
# mathematik.py
# Modul für allgemeine mathematische Berechnungen
PI = 3.141592653589793 # Hochpräziser Wert
def berechne(a: float, b: float) -> float:
"""Berechnet das Produkt zweier Zahlen."""
return a * b
def potenz(basis: float, exponent: int) -> float:
"""Berechnet basis hoch exponent."""
return basis**exponent

View File

@ -0,0 +1,59 @@
# Ziel dieses Skripts:
#
# Testergebnisse einer Klasse auswerten und formatiert ausgeben.
#
# Gegeben: Punkte von 5 Studierenden (von max. 100 Punkten)
# Erwartet:
# Mittelwert → 73.5 (kaufm. gerundet → 74 → "Gut")
# Max. Punkte → 100 (aus statistik.MAX_WERT)
#
# ----------------------------------------------------------------------
# AUFGABE: Das Skript gibt falsche Werte aus. Es gibt keine Fehlermeldung
# vom Interpreter. Finde heraus, warum die Ergebnisse falsch sind
# und korrigiere die Import-Anweisungen!
# ----------------------------------------------------------------------
from src.statistik import (
runde,
mittelwert,
bewerte,
MAX_WERT,
) # importiert: runde, mittelwert, bewerte, MAX_WERT, MIN_WERT
from src.formatierung import (
trennlinie,
) # importiert: runde, als_prozent, trennlinie, MAX_WERT, MIN_WERT
PUNKTE = [92, 85, 61, 48, 82]
def auswertung():
print(trennlinie("="))
print(" TESTERGEBNISSE")
print(trennlinie("="))
# --- Mittelwert berechnen und benoten ---
mw = mittelwert(PUNKTE)
mw_gerundet = runde(mw) # Soll kaufm. auf int runden → Erwartet: 74
note = bewerte(mw)
print(f" Mittelwert : {mw}")
print(f" Gerundet : {mw_gerundet} ← Erwartet: 74 (int)")
print(f" Durchschnittsnote: {note}")
print(trennlinie())
# --- Maximale Punktzahl aus dem Modul ---
print(f" Max. Punkte : {MAX_WERT} ← Erwartet: 100")
print(trennlinie())
# --- Einzelne Ergebnisse ---
print(" Einzelergebnisse:")
for p in PUNKTE:
print(f" {p:>3} Punkte → {bewerte(p)}")
print(trennlinie("="))
if __name__ == "__main__":
auswertung()

View File

@ -0,0 +1,21 @@
# formatierung.py
# Modul für Ausgabe-Formatierung und Darstellung
MAX_WERT = 255 # Maximaler RGB-Farbwert (0255)
MIN_WERT = 0 # Minimaler RGB-Farbwert
def runde(wert: float, stellen: int = 2) -> float:
"""Rundet auf 'stellen' Nachkommastellen (für Anzeige-Formatierung)."""
faktor = 10**stellen
return int(wert * faktor + 0.5) / faktor
def als_prozent(wert: float, gesamt: float) -> str:
"""Gibt einen Wert als formatierten Prozentstring zurück."""
return f"{runde(wert / gesamt * 100)} %"
def trennlinie(zeichen: str = "-", laenge: int = 40) -> str:
"""Erzeugt eine Trennlinie für die Konsolenausgabe."""
return zeichen * laenge

View File

@ -0,0 +1,30 @@
# statistik.py
# Modul für statistische Berechnungen
MAX_WERT = 100 # Maximale Punktzahl in einem Test (0100)
MIN_WERT = 0 # Minimale Punktzahl
def runde(wert: float) -> int:
"""Rundet kaufmännisch auf ganze Zahlen (für Testergebnisse)."""
return int(wert + 0.5)
def mittelwert(werte: list[float]) -> float:
"""Berechnet den arithmetischen Mittelwert einer Liste."""
return sum(werte) / len(werte)
def bewerte(punkte: float) -> str:
"""Gibt eine Note zurück basierend auf Punktzahl (0100)."""
p = runde(punkte)
if p >= 90:
return "Sehr gut"
elif p >= 75:
return "Gut"
elif p >= 60:
return "Befriedigend"
elif p >= 45:
return "Ausreichend"
else:
return "Ungenügend"

View File

@ -0,0 +1,27 @@
# random.py
#
# Dieses Modul wurde angelegt, um eigene Hilfsfunktionen für
# "Zufallsentscheidungen" in der Lotterie-Logik bereitzustellen.
#
# Es definiert einige der gleichen Funktionen wie das Standardmodul
# aber mit vereinfachter (fehlerhafter) Implementierung.
def randint(a: int, b: int) -> int:
"""Gibt eine 'zufällige' Ganzzahl zwischen a und b zurück."""
return a # ← gibt IMMER den kleinsten Wert zurück!
def shuffle(lst: list) -> None:
"""Mischt eine Liste in-place."""
pass # ← tut gar nichts!
def sample(population: list, k: int) -> list:
"""Gibt k zufällige Elemente aus population zurück."""
return list(population)[:k] # ← gibt IMMER die ersten k Elemente zurück!
def seed(a=None):
"""Setzt den Zufallsgenerator-Seed."""
pass # ← keine Wirkung

View File

@ -0,0 +1,51 @@
# Lotterie-Simulation: Zieht 6 aus 45 Zahlen, dazu eine Zusatzzahl.
#
# Wiederholt die Ziehung 5× und gibt die Ergebnisse aus.
# Anschließend wird die Gewinnerliste zufällig gemischt.
#
# Erwartetes Verhalten:
# • Jede Ziehung liefert 6 VERSCHIEDENE, ZUFÄLLIGE Zahlen aus 145
# • Wiederholte Aufrufe liefern UNTERSCHIEDLICHE Ergebnisse
# • Die Teilnehmerliste ist nach dem Mischen in ZUFÄLLIGER Reihenfolge
#
# ----------------------------------------------------------------------
# AUFGABE: Das Programm läuft ohne Fehlermeldung, aber die Ergebnisse
# sind offensichtlich nicht zufällig. Finde die Ursache und
# behebe den Fehler ohne ziehung.py oder statistik.py zu ändern!
#
# REMARK: Möglicherweise ist das Problem sogar abhängig von der verwendeten
# IDE... Lasst das main.py mal aus eurer IDE laufen sowie aus dem
# Terminal. Sind die Resultate gleich?
# ----------------------------------------------------------------------
from ziehung import ziehe_zahlen, ziehe_zusatzzahl
from statistik import simuliere_ziehungen, mische_teilnehmer
TEILNEHMER = ["Alice", "Bob", "Carol", "Dave", "Eve"]
def main():
print("=" * 45)
print(" LOTTO-SIMULATION (6 aus 45)")
print("=" * 45)
print("\n--- 5 unabhängige Ziehungen ---")
for i, ziehung in enumerate(simuliere_ziehungen(5), 1):
print(f" Ziehung {i}: {ziehung}")
print("\n--- Einzelziehung mit Zusatzzahl ---")
haupt = ziehe_zahlen()
zusatz = ziehe_zusatzzahl(haupt)
print(f" Hauptzahlen : {haupt}")
print(f" Zusatzzahl : {zusatz}")
print("\n--- Gewinnerliste (zufällig gemischt) ---")
gemischt = mische_teilnehmer(TEILNEHMER)
for rang, name in enumerate(gemischt, 1):
print(f" Rang {rang}: {name}")
print("=" * 45)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,20 @@
# Wertet mehrere Lottoziehungen statistisch aus
import custom_random
from ziehung import ziehe_zahlen
def simuliere_ziehungen(anzahl: int = 10) -> list[list[int]]:
"""Führt 'anzahl' unabhängige Lottoziehungen durch."""
ergebnisse = []
for _ in range(anzahl):
zahlen = ziehe_zahlen()
ergebnisse.append(zahlen)
return ergebnisse
def mische_teilnehmer(teilnehmer: list[str]) -> list[str]:
"""Mischt die Teilnehmerliste zufällig (z. B. für Gewinner-Reihenfolge)."""
kopie = teilnehmer[:]
custom_random.shuffle(kopie)
return kopie

View File

@ -0,0 +1,19 @@
# lotterie/ziehung.py
# Modul für die eigentliche Lottoziehung
import custom_random
ZAHLEN_POOL = list(range(1, 46)) # Zahlen 145
def ziehe_zahlen(anzahl: int = 6) -> list[int]:
"""Zieht 'anzahl' verschiedene Lottozahlen aus dem Pool."""
gezogen = custom_random.sample(ZAHLEN_POOL, anzahl)
gezogen.sort()
return gezogen
def ziehe_zusatzzahl(ausgeschlossen: list[int]) -> int:
"""Zieht eine Zusatzzahl, die nicht in der Hauptziehung vorkommt."""
pool = [z for z in ZAHLEN_POOL if z not in ausgeschlossen]
return custom_random.randint(1, len(pool) - 1) # ← Index in pool, dann Lookup

View File

@ -0,0 +1,53 @@
# Schulverwaltungs-Simulation:
# • Drei Schüler werden angelegt (mit Noten in zwei Kursen)
# • Zwei Kurse werden erstellt, Schüler werden eingeschrieben
# • Notenspiegel je Schüler und Kursbericht je Kurs wird ausgegeben
#
# ----------------------------------------------------------------------
# AUFGABE: Das Programm lässt sich gar nicht erst starten es gibt
# sofort einen ImportError.
#
# 1. Lies die Fehlermeldung sorgfältig und zeichne den
# Importgraphen auf: Welches Modul importiert welches?
#
# 2. Versuche durch eine geeignete Gegenmassnahme den Fehler zu
# beheben.
# ----------------------------------------------------------------------
from src.kurs import Kurs
from src.schueler import Schueler
def main():
# --- Schüler anlegen ---
anna = Schueler("Anna Meier", {"Mathematik": 2.5, "Deutsch": 3.0})
ben = Schueler("Ben Keller", {"Mathematik": 5.0, "Deutsch": 2.0})
clara = Schueler("Clara Huber", {"Mathematik": 3.5, "Deutsch": 4.5})
# --- Kurse anlegen und Schüler einschreiben ---
mathe = Kurs("Mathematik")
deutsch = Kurs("Deutsch")
for s in [anna, ben, clara]:
mathe.einschreiben(s)
deutsch.einschreiben(s)
# --- Ausgabe Notenspiegel ---
print("=" * 45)
print(" NOTENSPIEGEL")
print("=" * 45)
for s in [anna, ben, clara]:
print(s.notenspiegel())
print()
# --- Ausgabe Kursberichte ---
print("=" * 45)
print(" KURSBERICHTE")
print("=" * 45)
print(mathe.kursbericht())
print(deutsch.kursbericht())
print("=" * 45)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,16 @@
# ---------------------------------------------------------------
# Gemeinsam genutzte Hilfsfunktion wird auch in schueler.py
# über den Import von oben verwendet.
# ---------------------------------------------------------------
def ist_bestanden(note: float) -> bool:
"""Gibt True zurück, wenn die Note ≤ 4.0 (bestanden) ist."""
return note <= 4.0
def berechne_klassendurchschnitt(noten: list[float]) -> float:
"""Berechnet den Notendurchschnitt einer Gruppe."""
if not noten:
return 0.0
return round(sum(noten) / len(noten), 2)

View File

@ -0,0 +1,35 @@
# schulverwaltung/kurs.py
#
# Verwaltet Kurse und eingeschriebene Schüler.
# Benötigt die Schueler-Klasse aus schueler.py für die Einschreibung
# und Notenauswertung.
from notenrechner import ist_bestanden, berechne_klassendurchschnitt
from .schueler import Schueler
class Kurs:
def __init__(self, name: str):
self.name = name
self.schueler: list[Schueler] = []
def einschreiben(self, schueler: Schueler) -> None:
"""Schreibt einen Schüler in diesen Kurs ein."""
self.schueler.append(schueler)
def kursbericht(self) -> str:
"""Gibt eine Übersicht über alle Schüler und den Kursdurchschnitt aus."""
zeilen = [f"\n Kurs: {self.name} ({len(self.schueler)} Schüler)"]
noten_im_kurs = []
for s in self.schueler:
note = s.noten.get(self.name)
if note is not None:
status = "" if ist_bestanden(note) else ""
zeilen.append(f" {status} {s.name:<20} {note:.1f}")
noten_im_kurs.append(note)
if noten_im_kurs:
schnitt = berechne_klassendurchschnitt(noten_im_kurs)
zeilen.append(f" → Klassendurchschnitt: {schnitt:.2f}")
return "\n".join(zeilen)

View File

@ -0,0 +1,28 @@
# Verwaltet Schüler-Daten und prüft den Abschlussstatus.
# Benötigt ist_bestanden() aus kurs.py, um festzustellen ob
# ein Schüler alle Kurse bestanden hat.
from notenrechner import ist_bestanden
class Schueler:
def __init__(self, name: str, noten: dict[str, float]):
"""
name : Vollständiger Name des Schülers
noten : {Kursname: Note} z. B. {'Mathematik': 4.5, 'Deutsch': 3.0}
"""
self.name = name
self.noten = noten
def kann_abschliessen(self) -> bool:
"""Gibt True zurück, wenn alle Kursnoten bestanden sind (Note ≤ 4.0)."""
return all(ist_bestanden(note) for note in self.noten.values())
def notenspiegel(self) -> str:
"""Gibt eine formatierte Übersicht aller Noten zurück."""
zeilen = [f" Schüler: {self.name}"]
for kurs, note in self.noten.items():
status = "" if ist_bestanden(note) else ""
zeilen.append(f" {status} {kurs:<18} Note: {note:.1f}")
zeilen.append(f" → Abschluss möglich: {self.kann_abschliessen()}")
return "\n".join(zeilen)

View File

@ -1,2 +1,6 @@
def discount_price(price: float, percent: float) -> float:
if price < 0:
raise ValueError("Price must be >= 0")
if not 0 <= percent <= 100:
raise ValueError("Percent must be between 0 and 100")
return price - price * percent / 100

View File

@ -0,0 +1,17 @@
from src.codewars.kata_building_blocks import Block
def test_block():
b1 = Block([2, 2, 2])
b2 = Block([41, 87, 67])
assert b1.get_width() == 2
assert b1.get_length() == 2
assert b1.get_height() == 2
assert b1.get_volume() == 8
assert b1.get_surface_area() == 24
assert b2.get_width() == 41
assert b2.get_length() == 87
assert b2.get_height() == 67
assert b2.get_volume() == 238989
assert b2.get_surface_area() == 24286

View File

@ -0,0 +1,8 @@
from src.codewars.kata_ceasar_cipher_helper import CaesarCipher
def test_cipher_helper():
c = CaesarCipher(5)
assert c.encode("Codewars") == "HTIJBFWX"
assert c.decode("HTIJBFWX") == "CODEWARS"

View File

@ -0,0 +1,15 @@
from src.codewars.kata_chinese_numeral_encoder import to_chinese_numeral
def test_to_chinese():
assert to_chinese_numeral(9) == ""
assert to_chinese_numeral(-5) == "负五"
assert to_chinese_numeral(0.5) == "零点五"
assert to_chinese_numeral(10) == ""
assert to_chinese_numeral(110) == "一百一十"
assert to_chinese_numeral(111) == "一百一十一"
assert to_chinese_numeral(1000) == "一千"
assert to_chinese_numeral(10000) == "一万"
assert to_chinese_numeral(10006) == "一万零六"
assert to_chinese_numeral(10306.005) == "一万零三百零六点零零五"
assert to_chinese_numeral(-10.000001) == "负十点零零零零零一"

View File

@ -0,0 +1,7 @@
from src.codewars.kata_force_of_gravity import solution
def test_a():
assert solution([1000, 1000, 100], ["g", "kg", "m"]) == 6.67e-12
assert solution([1000, 1000, 100], ["kg", "kg", "m"]) == 6.6699999999999995e-09
assert solution([1000, 1000, 100], ["kg", "kg", "cm"]) == 0.0000667

View File

@ -0,0 +1,15 @@
from src.codewars.kata_next_bigger_number_same_digits import next_bigger
def test_next_bigger():
assert next_bigger(12) == 21
assert next_bigger(21) == -1
assert next_bigger(513) == 531
assert next_bigger(2017) == 2071
assert next_bigger(414) == 441
assert next_bigger(144) == 414
assert next_bigger(1234567890) == 1234567908
assert next_bigger(59884848459853) == 59884848483559
assert next_bigger(7600201336) == 7600201363
assert next_bigger(5113455566888) == 5113455568688
assert next_bigger(4769560370633) == 4769560373036

View File

@ -0,0 +1,15 @@
from src.codewars.kata_object_oriented_piracy import Ship
def test_piracy():
empty_ship = Ship(0, 0)
assert not empty_ship.is_worth_it()
boat = Ship(15, 20)
assert not boat.is_worth_it()
worthy_ship = Ship(100, 20)
assert worthy_ship.is_worth_it()
big_boat = Ship(35, 20)
assert not big_boat.is_worth_it()

View File

@ -0,0 +1,27 @@
from src.codewars.kata_pagination_helper import PaginationHelper
def test_pagination():
collection = ["a", "b", "c", "d", "e", "f"]
helper = PaginationHelper(collection, 4)
assert helper.page_count() == 2
assert helper.item_count() == 6
assert helper.page_item_count(0) == 4
assert helper.page_item_count(1) == 2
assert helper.page_item_count(2) == -1
assert helper.page_index(5) == 1
assert helper.page_index(2) == 0
assert helper.page_index(20) == -1
assert helper.page_index(-10) == -1
empty = PaginationHelper([], 10)
assert empty.item_count() == 0
assert empty.page_count() == 0
assert empty.page_index(0) == -1
assert empty.page_index(1) == -1
assert empty.page_index(-1) == -1
assert empty.page_item_count(0) == -1
assert empty.page_item_count(1) == -1
assert empty.page_item_count(-1) == -1

View File

@ -0,0 +1,10 @@
from src.codewars.kata_snail import snail
def test_snail():
array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
assert snail(array) == [1, 2, 3, 6, 9, 8, 7, 4, 5]
array = [[1, 2, 3], [8, 9, 4], [7, 6, 5]]
assert snail(array) == [1, 2, 3, 4, 5, 6, 7, 8, 9]

View File

@ -0,0 +1,13 @@
from src.codewars.kata_the_lamp import Lamp
def test_lamp():
my_lamp = Lamp("Blue")
assert my_lamp.color == "Blue"
assert not my_lamp.on
assert my_lamp.state() == "The lamp is off."
my_lamp.toggle_switch()
assert my_lamp.state() == "The lamp is on."
my_lamp.toggle_switch()
assert my_lamp.state() == "The lamp is off."

View File

@ -0,0 +1,15 @@
from src.codewars.kata_thinkful_quarks import Quark
def test_quark():
q1 = Quark("red", "up")
q2 = Quark("blue", "strange")
assert q1.color == "red"
assert q2.flavor == "strange"
assert q2.baryon_number == 1 / 3
q1.interact(q2)
assert q1.color == "blue"
assert q2.color == "red"

View File

@ -0,0 +1,13 @@
from src.codewars.kata_thinkful_vectors import Vector
def test_vector():
v1 = Vector(3, 4)
v2 = Vector(1, 2)
assert v1.x == 3
assert v2.y == 2
assert hasattr(v1, "add")
assert isinstance(v1.add(v2), Vector)
assert (v1.add(v2)).x == 4
assert (v1.add(v2)).y == 6

View File

@ -0,0 +1,7 @@
from src.codewars.kata_version_mamanger import VersionManager
def test_lamp():
v = VersionManager("1.1.1")
assert v.release() == "1.1.1"

View File

@ -0,0 +1,16 @@
from src.codewars.kata_vigenere_cipher_helper import VigenereCipher
def test_cipher_helper():
abc = "abcdefghijklmnopqrstuvwxyz"
key = "password"
c = VigenereCipher(key, abc)
assert c.encode("codewars") == "rovwsoiv"
assert c.decode("rovwsoiv") == "codewars"
assert c.encode("waffles") == "laxxhsj"
assert c.decode("laxxhsj") == "waffles"
assert c.encode("CODEWARS") == "CODEWARS"
assert c.decode("CODEWARS") == "CODEWARS"

View File

@ -0,0 +1,15 @@
from src.codewars.kata_who_the_most_money import Student, most_money
def test_most_money():
phil = Student("Phil", 2, 2, 1)
cam = Student("Cameron", 2, 2, 0)
geoff = Student("Geoff", 0, 3, 0)
assert most_money([cam, geoff, phil]) == "Phil"
phil = Student("Phil", 2, 2, 2)
cam = Student("Cameron", 2, 2, 2)
geoff = Student("Geoff", 2, 2, 2)
assert most_money([cam, geoff, phil]) == "all"

View File

@ -1,6 +1,21 @@
from src.tutorial.testing.shop.pricing import discount_price
import pytest
def test_discount_price_reduces_price():
result = discount_price(100.0, 20.0)
assert result == 80
assert discount_price(100.0, 50.0) == 50
assert discount_price(100.0, 0) == 100
assert discount_price(100.0, 100) == 0
def test_negative_orice_raises_value_error() -> None:
with pytest.raises(ValueError):
discount_price(-100.0, 50.0) is ValueError
def test_percent_above_hundered_message() -> None:
with pytest.raises(ValueError, match="between 0 and 100"):
discount_price(100.0, 150.0)