From 18a1fa9bdff979687fa7bbdbacf7dd013179324b Mon Sep 17 00:00:00 2001 From: toddmath Date: Wed, 27 May 2026 13:32:34 -0700 Subject: [PATCH 001/113] Improve code quality, documentation, and type safety Enables basic type checking in VS Code and adds comprehensive docstrings to classes and methods to improve maintainability and clarity. Refines error handling by catching more specific exceptions and avoids shadowing built-in names. Also simplifies factory logic and updates project configuration for better consistency. --- .vscode/settings.json | 3 ++- 2025/go/error_handling.py | 19 +++++++------------ 2025/simple/before.py | 22 +++++++++++++++++++--- 2025/simple/notification_fn.py | 11 +++++++++++ 2025/typehints/generic_return.py | 2 ++ 2025/typehints/pyproject.toml | 8 +++----- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index bb99ce25..8773355b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "python.testing.pytestEnabled": true, // "python.testing.unittestEnabled": false, "python.testing.cwd": "${workspaceFolder}/tests", - "python.defaultInterpreterPath": "${workspaceFolder}/2025/typescript/lokalise/.venv/bin/python" + "python.defaultInterpreterPath": "${workspaceFolder}/2025/typescript/lokalise/.venv/bin/python", + "python.analysis.typeCheckingMode": "basic" } \ No newline at end of file diff --git a/2025/go/error_handling.py b/2025/go/error_handling.py index f900baaf..e48e0b5e 100644 --- a/2025/go/error_handling.py +++ b/2025/go/error_handling.py @@ -1,20 +1,20 @@ def read_and_sum_integers(file_path): try: - with open(file_path, "r") as file: - sum = 0 + with open(file_path, "r", encoding="utf-8") as file: + total = 0 for line in file: try: num = int(line.strip()) - sum += num + total += num except ValueError as e: raise ValueError( f"Invalid content in file: '{line.strip()}'" ) from e - return sum + return total except FileNotFoundError as e: raise FileNotFoundError(f"File not found: {file_path}") from e - except Exception as e: - raise RuntimeError(f"An unexpected error occurred: {e}") from e + except OSError as e: + raise RuntimeError(f"An unexpected OS error occurred: {e}") from e def main(): @@ -23,13 +23,8 @@ def main(): try: total = read_and_sum_integers(file_path) print(f"Sum of integers: {total}") - except FileNotFoundError as e: - print(f"Error: {e}") - except ValueError as e: + except (FileNotFoundError, ValueError, RuntimeError) as e: print(f"Error: {e}") - except Exception as e: - print(f"Unexpected error: {e}") - if __name__ == "__main__": main() diff --git a/2025/simple/before.py b/2025/simple/before.py index c8e5c61c..f55cab0c 100644 --- a/2025/simple/before.py +++ b/2025/simple/before.py @@ -33,12 +33,14 @@ def take_a_holiday(self, payout: bool) -> None: @dataclass class HourlyEmployee(Employee): + """Represents an hourly employee.""" hourly_rate: float = 50 amount: int = 10 @dataclass class SalariedEmployee(Employee): + """Represents a salaried employee who receives a fixed monthly salary payment.""" monthly_salary: float = 5000 @@ -61,18 +63,23 @@ def __init__(self) -> None: self.employees: list[Employee] = [] def add_employee(self, employee: Employee) -> None: + """Add an employee to the company.""" self.employees.append(employee) def find_managers(self) -> list[Employee]: + """Find all employees with the role of manager.""" return [e for e in self.employees if e.role == "manager"] def find_vice_presidents(self) -> list[Employee]: + """Find all employees with the role of vice-president.""" return [e for e in self.employees if e.role == "vice-president"] def find_support_staff(self) -> list[Employee]: + """Find all employees with the role of support.""" return [e for e in self.employees if e.role == "support"] def pay_employee(self, employee: Employee) -> None: + """Pay an employee.""" if isinstance(employee, SalariedEmployee): print( f"Paying employee {employee.name} a monthly salary of ${employee.monthly_salary}." @@ -100,6 +107,10 @@ class Notification(ABC): def send(self, employee: Employee, message: str) -> None: pass + @abstractmethod + def is_enabled(self) -> bool: + """Check if notifications are enabled.""" + class EmailNotification(Notification): """Email notification implementation.""" @@ -107,6 +118,9 @@ class EmailNotification(Notification): def send(self, employee: Employee, message: str) -> None: print(f"Sending email to {employee.name}: {message}") + def is_enabled(self) -> bool: + return True + class SMSNotification(Notification): """SMS notification implementation.""" @@ -114,6 +128,9 @@ class SMSNotification(Notification): def send(self, employee: Employee, message: str) -> None: print(f"Sending SMS to {employee.name}: {message}") + def is_enabled(self) -> bool: + return True + class NotificationFactory: """Factory for creating notifications.""" @@ -122,10 +139,9 @@ class NotificationFactory: def get_notification(method: str) -> Notification: if method == "email": return EmailNotification() - elif method == "sms": + if method == "sms": return SMSNotification() - else: - raise ValueError("Invalid notification method") + raise ValueError("Invalid notification method") def main() -> None: diff --git a/2025/simple/notification_fn.py b/2025/simple/notification_fn.py index f1ce958c..154140d7 100644 --- a/2025/simple/notification_fn.py +++ b/2025/simple/notification_fn.py @@ -32,17 +32,20 @@ def take_a_holiday(self, payout: bool) -> None: @dataclass class HourlyEmployee(Employee): + """Represents an hourly employee.""" hourly_rate: float = 50 amount: int = 10 @dataclass class SalariedEmployee(Employee): + """Represents a salaried employee who receives a fixed monthly salary payment.""" monthly_salary: float = 5000 @dataclass class Freelancer(Employee): + """Represents a freelancer who receives a fixed hourly rate for a fixed amount of hours.""" hourly_rate: float = 50 amount: int = 10 retainer: float = 1000 @@ -50,6 +53,7 @@ class Freelancer(Employee): @dataclass class Intern(Employee): + """Represents an intern who receives a fixed monthly salary payment.""" monthly_salary: float = 1000 @@ -60,18 +64,23 @@ def __init__(self) -> None: self.employees: list[Employee] = [] def add_employee(self, employee: Employee) -> None: + """Add an employee to the company.""" self.employees.append(employee) def find_managers(self) -> list[Employee]: + """Find all employees with the role of manager.""" return [e for e in self.employees if e.role == "manager"] def find_vice_presidents(self) -> list[Employee]: + """Find all employees with the role of vice-president.""" return [e for e in self.employees if e.role == "vice-president"] def find_support_staff(self) -> list[Employee]: + """Find all employees with the role of support.""" return [e for e in self.employees if e.role == "support"] def pay_employee(self, employee: Employee) -> None: + """Pay an employee.""" if isinstance(employee, SalariedEmployee): print( f"Paying employee {employee.name} a monthly salary of ${employee.monthly_salary}." @@ -93,10 +102,12 @@ def pay_employee(self, employee: Employee) -> None: def send_email(employee: Employee, message: str) -> None: + """Send an email to an employee.""" print(f"Sending email to {employee.name}: {message}") def send_sms(employee: Employee, message: str) -> None: + """Send an SMS to an employee.""" print(f"Sending SMS to {employee.name}: {message}") diff --git a/2025/typehints/generic_return.py b/2025/typehints/generic_return.py index f071c3e5..ff411fe2 100644 --- a/2025/typehints/generic_return.py +++ b/2025/typehints/generic_return.py @@ -9,7 +9,9 @@ def main() -> None: items = [100, 200, 300] discount = 0.2 discounted_items = calculate_discounts(items, discount) + # type: ignore print(len(discounted_items)) # type issue here + # type: ignore print(discounted_items[0]) # type issue here diff --git a/2025/typehints/pyproject.toml b/2025/typehints/pyproject.toml index 002780a8..7417f176 100644 --- a/2025/typehints/pyproject.toml +++ b/2025/typehints/pyproject.toml @@ -1,12 +1,10 @@ [project] name = "typehints" version = "0.0.1" -authors = [{name = "ArjanCodes"}] -license = {text = "MIT"} +authors = [{ name = "ArjanCodes" }] +license = { text = "MIT" } requires-python = ">=3.12" -dependencies = [ - "mypy>=1.15.0", -] +dependencies = ["mypy>=1.15.0"] [tool.pytest.ini_options] pythonpath = "src" From b0c7051f429d5ddb950435ea3955b706a36cc775 Mon Sep 17 00:00:00 2001 From: toddmath Date: Wed, 27 May 2026 14:00:35 -0700 Subject: [PATCH 002/113] fix python ide config, remove unused imports --- .vscode/settings.json | 18 +++++++++--------- 2022/async/after-1/iot/service.py | 2 +- 2022/async/after-2/iot/service.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8773355b..ce291017 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,10 @@ { - // Python settings - // "python.envFile": "${workspaceFolder}/.env",, - // Test settings - "python.testing.pytestEnabled": true, - // "python.testing.unittestEnabled": false, - "python.testing.cwd": "${workspaceFolder}/tests", - "python.defaultInterpreterPath": "${workspaceFolder}/2025/typescript/lokalise/.venv/bin/python", - "python.analysis.typeCheckingMode": "basic" -} \ No newline at end of file + // Python settings + // "python.envFile": "${workspaceFolder}/.env",, + // Test settings + "python.testing.pytestEnabled": true, + // "python.testing.unittestEnabled": false, + "python.testing.cwd": "${workspaceFolder}/tests", + "python.defaultInterpreterPath": "/Users/todd/.pyenv/shims/python", + "python.analysis.typeCheckingMode": "basic" +} diff --git a/2022/async/after-1/iot/service.py b/2022/async/after-1/iot/service.py index 22040d13..b7e70678 100644 --- a/2022/async/after-1/iot/service.py +++ b/2022/async/after-1/iot/service.py @@ -1,4 +1,4 @@ -import asyncio +# import asyncio import random import string from typing import Protocol diff --git a/2022/async/after-2/iot/service.py b/2022/async/after-2/iot/service.py index bacc2775..d7daa9cd 100644 --- a/2022/async/after-2/iot/service.py +++ b/2022/async/after-2/iot/service.py @@ -1,7 +1,7 @@ import asyncio import random import string -from typing import Any, Awaitable, Protocol +from typing import Protocol from iot.message import Message, MessageType From f2135447acf063b23a6b2cfc22b33091b1a30368 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 28 Apr 2025 12:54:02 +0200 Subject: [PATCH 003/113] Added new class example --- 2025/functions/new_class_type.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 2025/functions/new_class_type.py diff --git a/2025/functions/new_class_type.py b/2025/functions/new_class_type.py new file mode 100644 index 00000000..dd10133b --- /dev/null +++ b/2025/functions/new_class_type.py @@ -0,0 +1,11 @@ +import types + +# Dynamically create a class with a __call__ method +DynamicFunction = types.new_class( + "DynamicFunction", + (), + {}, + exec_body=lambda ns: ns.update({"__call__": lambda self, x: x * 2}), +) +double = DynamicFunction() +print(double(5)) # 10 From 04b3ae2f32909a9b95d6128a9617a5d614adb709 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 29 Apr 2025 14:46:54 +0200 Subject: [PATCH 004/113] Added function examples --- 2025/functions/1_lambda.py | 7 +++ 2025/functions/2_partial.py | 15 ++++++ 2025/functions/3_decorator.py | 30 ++++++++++++ 2025/functions/4_call_dunder.py | 12 +++++ 2025/functions/5_exec.py | 8 ++++ 2025/functions/6_eval.py | 7 +++ ...{new_class_type.py => 7_new_class_type.py} | 0 2025/functions/8_code_type.py | 47 +++++++++++++++++++ 2025/functions/pyproject.toml | 6 +++ 2025/functions/uv.lock | 8 ++++ 10 files changed, 140 insertions(+) create mode 100644 2025/functions/1_lambda.py create mode 100644 2025/functions/2_partial.py create mode 100644 2025/functions/3_decorator.py create mode 100644 2025/functions/4_call_dunder.py create mode 100644 2025/functions/5_exec.py create mode 100644 2025/functions/6_eval.py rename 2025/functions/{new_class_type.py => 7_new_class_type.py} (100%) create mode 100644 2025/functions/8_code_type.py create mode 100644 2025/functions/pyproject.toml create mode 100644 2025/functions/uv.lock diff --git a/2025/functions/1_lambda.py b/2025/functions/1_lambda.py new file mode 100644 index 00000000..785c2362 --- /dev/null +++ b/2025/functions/1_lambda.py @@ -0,0 +1,7 @@ +def main() -> None: + greet = lambda name: f"Hello, {name}!" + print(greet("Alice")) # "Hello, Alice!" + + +if __name__ == "__main__": + main() diff --git a/2025/functions/2_partial.py b/2025/functions/2_partial.py new file mode 100644 index 00000000..f3338bee --- /dev/null +++ b/2025/functions/2_partial.py @@ -0,0 +1,15 @@ +from functools import partial + + +def power(base: float, exponent: float) -> float: + return base**exponent + + +def main() -> None: + # Create a partial function that always raises to the power of 2 + square = partial(power, exponent=2) + print(square(4)) # 16 + + +if __name__ == "__main__": + main() diff --git a/2025/functions/3_decorator.py b/2025/functions/3_decorator.py new file mode 100644 index 00000000..72588c0b --- /dev/null +++ b/2025/functions/3_decorator.py @@ -0,0 +1,30 @@ +from functools import wraps +from typing import Any, Callable + + +def print_result( + fmt: str = "Result: {}", +) -> Callable[[Callable[[Any], Any]], Callable[[Any], Any]]: + def decorator(func: Callable[[Any], Any]) -> Callable[[Any], Any]: + @wraps(func) + def wrapper(x: Any) -> Any: + result = func(x) + print(fmt.format(result)) + return result + + return wrapper + + return decorator + + +@print_result(fmt="Computed value => {}") +def double(x: int) -> int: + return x * 2 + + +def main() -> None: + double(5) + + +if __name__ == "__main__": + main() diff --git a/2025/functions/4_call_dunder.py b/2025/functions/4_call_dunder.py new file mode 100644 index 00000000..15e88b44 --- /dev/null +++ b/2025/functions/4_call_dunder.py @@ -0,0 +1,12 @@ +class Greeter: + def __call__(self, name: str) -> str: + return f"Hello, {name}!" + + +def main() -> None: + greet = Greeter() + print(greet("Alice")) # Output: "Hello, Alice!" + + +if __name__ == "__main__": + main() diff --git a/2025/functions/5_exec.py b/2025/functions/5_exec.py new file mode 100644 index 00000000..f603d173 --- /dev/null +++ b/2025/functions/5_exec.py @@ -0,0 +1,8 @@ +def main() -> None: + exec("def add(x, y): return x + y") + + print(add(3, 4)) # 7 + + +if __name__ == "__main__": + main() diff --git a/2025/functions/6_eval.py b/2025/functions/6_eval.py new file mode 100644 index 00000000..d2fd86ef --- /dev/null +++ b/2025/functions/6_eval.py @@ -0,0 +1,7 @@ +def main() -> None: + func = eval("lambda x: x * 2") + print(func(5)) # 10 + + +if __name__ == "__main__": + main() diff --git a/2025/functions/new_class_type.py b/2025/functions/7_new_class_type.py similarity index 100% rename from 2025/functions/new_class_type.py rename to 2025/functions/7_new_class_type.py diff --git a/2025/functions/8_code_type.py b/2025/functions/8_code_type.py new file mode 100644 index 00000000..ca8a8c0c --- /dev/null +++ b/2025/functions/8_code_type.py @@ -0,0 +1,47 @@ +import types + +# 3.10 Bytecode: +# 0 LOAD_FAST 0 (x) +# 2 LOAD_CONST 1 (42) +# 4 BINARY_ADD +# 6 RETURN_VALUE + +bytecode = bytes( + [ + 124, + 0, # LOAD_FAST x (arg 0) + 100, + 1, # LOAD_CONST 42 (arg 1) + 23, + 0, # BINARY_ADD + 83, + 0, # RETURN_VALUE + ] +) + +constants = (None, 42) +varnames = ("x",) + +# Create a CodeType for 3.10 +code = types.CodeType( + 1, # co_argcount + 0, # co_posonlyargcount + 0, # co_kwonlyargcount + 1, # co_nlocals + 2, # co_stacksize + 0x43, # co_flags (OPTIMIZED | NEWLOCALS | NOFREE) + bytecode, # co_code + constants, # co_consts + (), # co_names + varnames, # co_varnames + "", # co_filename + "add_42", # co_name + 1, # co_firstlineno + b"\x00\x01", # co_lnotab + (), # co_freevars + (), # co_cellvars +) + +func = types.FunctionType(code, globals(), "add_42") + +print(func(5)) # โœ… Output: 47 diff --git a/2025/functions/pyproject.toml b/2025/functions/pyproject.toml new file mode 100644 index 00000000..1ec9234a --- /dev/null +++ b/2025/functions/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "functions" +version = "0.1.0" +requires-python = ">=3.10,<3.11" +dependencies = [ +] diff --git a/2025/functions/uv.lock b/2025/functions/uv.lock new file mode 100644 index 00000000..a8b8ebc5 --- /dev/null +++ b/2025/functions/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = "==3.10.*" + +[[package]] +name = "functions" +version = "0.1.0" +source = { virtual = "." } From 807ce579f52f1df155acff77355a9d94f8f6faa4 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 25 Apr 2025 09:22:38 +0200 Subject: [PATCH 005/113] Added a few examples --- 2025/map/basic_loop.py | 26 ++++++++++++++++++++++++++ 2025/map/performance.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 2025/map/basic_loop.py create mode 100644 2025/map/performance.py diff --git a/2025/map/basic_loop.py b/2025/map/basic_loop.py new file mode 100644 index 00000000..9d7dd830 --- /dev/null +++ b/2025/map/basic_loop.py @@ -0,0 +1,26 @@ +from typing import TypedDict + + +class User(TypedDict): + name: str + age: int + + +def main() -> None: + users: list[User] = [ + {"name": "Alice", "age": 28}, + {"name": "Bob", "age": 17}, + {"name": "Carol", "age": 35}, + ] + + adult_names: list[str] = [] + + for user in users: + if user["age"] >= 18: + adult_names.append(user["name"].upper()) + + print(adult_names) + + +if __name__ == "__main__": + main() diff --git a/2025/map/performance.py b/2025/map/performance.py new file mode 100644 index 00000000..2cd2f9d2 --- /dev/null +++ b/2025/map/performance.py @@ -0,0 +1,41 @@ +import time +from typing import TypedDict + + +# Setup +class User(TypedDict): + name: str + age: int + + +def main() -> None: + users: list[User] = [ + {"name": "User" + str(i), "age": i % 50} + for i in range(10_000_000) # Increased size + ] + + # For loop + start: float = time.perf_counter() + adult_names: list[str] = [] + for user in users: + if user["age"] >= 18: + adult_names.append(user["name"].upper()) + end: float = time.perf_counter() + print("For loop:", end - start) + + # Map/filter + start = time.perf_counter() + adult_users = filter(lambda u: u["age"] >= 18, users) + adult_names = list(map(lambda u: u["name"].upper(), adult_users)) + end = time.perf_counter() + print("Map/filter:", end - start) + + # List comprehension + start = time.perf_counter() + adult_names = [user["name"].upper() for user in users if user["age"] >= 18] + end = time.perf_counter() + print("List comp:", end - start) + + +if __name__ == "__main__": + main() From e5c7151aa2e464168d87bd7bb31af9d1132b1f94 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 25 Apr 2025 17:08:30 +0200 Subject: [PATCH 006/113] Expanded map examples. --- 2025/map/{basic_loop.py => basic_example.py} | 0 2025/map/basic_example_comp.py | 26 ++++++++++++++++ 2025/map/basic_example_map.py | 23 +++++++++++++++ 2025/map/{performance.py => benchmark.py} | 4 +-- 2025/map/complex_logic.py | 31 ++++++++++++++++++++ 2025/map/complex_logic_comp.py | 20 +++++++++++++ 2025/map/complex_logic_map.py | 25 ++++++++++++++++ 2025/map/early_exit.py | 25 ++++++++++++++++ 2025/map/early_exit_filter.py | 24 +++++++++++++++ 2025/map/exception_handling.py | 19 ++++++++++++ 2025/map/exception_handling_map.py | 19 ++++++++++++ 2025/map/side_effects.py | 20 +++++++++++++ 2025/map/side_effects_map.py | 22 ++++++++++++++ warnings.txt | 3 ++ 14 files changed, 259 insertions(+), 2 deletions(-) rename 2025/map/{basic_loop.py => basic_example.py} (100%) create mode 100644 2025/map/basic_example_comp.py create mode 100644 2025/map/basic_example_map.py rename 2025/map/{performance.py => benchmark.py} (92%) create mode 100644 2025/map/complex_logic.py create mode 100644 2025/map/complex_logic_comp.py create mode 100644 2025/map/complex_logic_map.py create mode 100644 2025/map/early_exit.py create mode 100644 2025/map/early_exit_filter.py create mode 100644 2025/map/exception_handling.py create mode 100644 2025/map/exception_handling_map.py create mode 100644 2025/map/side_effects.py create mode 100644 2025/map/side_effects_map.py create mode 100644 warnings.txt diff --git a/2025/map/basic_loop.py b/2025/map/basic_example.py similarity index 100% rename from 2025/map/basic_loop.py rename to 2025/map/basic_example.py diff --git a/2025/map/basic_example_comp.py b/2025/map/basic_example_comp.py new file mode 100644 index 00000000..60569fa3 --- /dev/null +++ b/2025/map/basic_example_comp.py @@ -0,0 +1,26 @@ +from typing import TypedDict + + +class User(TypedDict): + name: str + age: int + + +def main() -> None: + users: list[User] = [ + {"name": "Alice", "age": 28}, + {"name": "Bob", "age": 17}, + {"name": "Carol", "age": 35}, + ] + + adult_names: list[str] = [ + + for user in users: + if user["age"] >= 18: + adult_names.append(user["name"].upper()) + + print(adult_names) + + +if __name__ == "__main__": + main() diff --git a/2025/map/basic_example_map.py b/2025/map/basic_example_map.py new file mode 100644 index 00000000..5e792a66 --- /dev/null +++ b/2025/map/basic_example_map.py @@ -0,0 +1,23 @@ +from typing import TypedDict + + +class User(TypedDict): + name: str + age: int + + +def main() -> None: + users: list[User] = [ + {"name": "Alice", "age": 28}, + {"name": "Bob", "age": 17}, + {"name": "Carol", "age": 35}, + ] + + adult_users = filter(lambda u: u["age"] >= 18, users) + adult_names = list(map(lambda u: u["name"].upper(), adult_users)) + + print(adult_names) + + +if __name__ == "__main__": + main() diff --git a/2025/map/performance.py b/2025/map/benchmark.py similarity index 92% rename from 2025/map/performance.py rename to 2025/map/benchmark.py index 2cd2f9d2..2877ed70 100644 --- a/2025/map/performance.py +++ b/2025/map/benchmark.py @@ -15,12 +15,12 @@ def main() -> None: ] # For loop - start: float = time.perf_counter() + start = time.perf_counter() adult_names: list[str] = [] for user in users: if user["age"] >= 18: adult_names.append(user["name"].upper()) - end: float = time.perf_counter() + end = time.perf_counter() print("For loop:", end - start) # Map/filter diff --git a/2025/map/complex_logic.py b/2025/map/complex_logic.py new file mode 100644 index 00000000..6124aefe --- /dev/null +++ b/2025/map/complex_logic.py @@ -0,0 +1,31 @@ +from typing import Any + + +def transform(data: list[dict[str, Any]]) -> list[int]: + result: list[int] = [] + for item in data: + if item["flag"]: + try: + val = item["a"]["b"].get("c", {}).get("d", 0) + result.append(val + 5) + except KeyError: + result.append(-1) + else: + result.append(-1) + return result + + +def main() -> None: + data: list[dict[str, Any]] = [ + {"flag": True, "a": {"b": {"c": {"d": 1}}}}, + {"flag": False, "a": {"b": {"c": {"d": 2}}}}, + {"flag": True, "a": {"b": {"c": {}}}}, + {"flag": True, "a": {"b": {}}}, + ] + + result = transform(data) + print(result) # Output: [6, -1, 5, 5] + + +if __name__ == "__main__": + main() diff --git a/2025/map/complex_logic_comp.py b/2025/map/complex_logic_comp.py new file mode 100644 index 00000000..3fc9fc0a --- /dev/null +++ b/2025/map/complex_logic_comp.py @@ -0,0 +1,20 @@ +from typing import Any + + +def transform(data: list[dict[str, Any]]) -> list[int]: + return [x["a"]["b"].get("c", {}).get("d", 0) + 5 if x["flag"] else -1 for x in data] + + +def main() -> None: + data: list[dict[str, Any]] = [ + {"flag": True, "a": {"b": {"c": {"d": 1}}}}, + {"flag": False, "a": {"b": {"c": {"d": 2}}}}, + {"flag": True, "a": {"b": {"c": {}}}}, + {"flag": True, "a": {"b": {}}}, + ] + result = transform(data) + print(result) # Output: [6, -1, 5, 5] + + +if __name__ == "__main__": + main() diff --git a/2025/map/complex_logic_map.py b/2025/map/complex_logic_map.py new file mode 100644 index 00000000..b02dd59d --- /dev/null +++ b/2025/map/complex_logic_map.py @@ -0,0 +1,25 @@ +from typing import Any + + +def transform(data: list[dict[str, Any]]) -> list[int]: + return list( + map( + lambda x: x["a"]["b"].get("c", {}).get("d", 0) + 5 if x["flag"] else -1, + data, + ) + ) + + +def main() -> None: + data: list[dict[str, Any]] = [ + {"flag": True, "a": {"b": {"c": {"d": 1}}}}, + {"flag": False, "a": {"b": {"c": {"d": 2}}}}, + {"flag": True, "a": {"b": {"c": {}}}}, + {"flag": True, "a": {"b": {}}}, + ] + result = transform(data) + print(result) # Output: [6, -1, 5, 5] + + +if __name__ == "__main__": + main() diff --git a/2025/map/early_exit.py b/2025/map/early_exit.py new file mode 100644 index 00000000..10a36eaf --- /dev/null +++ b/2025/map/early_exit.py @@ -0,0 +1,25 @@ +def scan_for_suspicious(events: list[dict[str, str]]) -> None: + for event in events: + if is_suspicious(event): + print(f"Suspicious login detected: {event}") + break + + +def is_suspicious(event: dict[str, str]) -> bool: + return ( + "login" in event.get("type", "").lower() + and "unusual" in event.get("details", "").lower() + ) + + +def main() -> None: + events: list[dict[str, str]] = [ + {"type": "login", "details": "User logged in"}, + {"type": "login", "details": "Unusual login from new device"}, + {"type": "logout", "details": "User logged out"}, + ] + scan_for_suspicious(events) + + +if __name__ == "__main__": + main() diff --git a/2025/map/early_exit_filter.py b/2025/map/early_exit_filter.py new file mode 100644 index 00000000..dd1a6949 --- /dev/null +++ b/2025/map/early_exit_filter.py @@ -0,0 +1,24 @@ +def scan_for_suspicious(events: list[dict[str, str]]) -> None: + event = next(filter(is_suspicious, events), None) + if event: + print(f"Suspicious login detected: {event}") + + +def is_suspicious(event: dict[str, str]) -> bool: + return ( + "login" in event.get("type", "").lower() + and "unusual" in event.get("details", "").lower() + ) + + +def main() -> None: + events: list[dict[str, str]] = [ + {"type": "login", "details": "User logged in"}, + {"type": "login", "details": "Unusual login from new device"}, + {"type": "logout", "details": "User logged out"}, + ] + scan_for_suspicious(events) + + +if __name__ == "__main__": + main() diff --git a/2025/map/exception_handling.py b/2025/map/exception_handling.py new file mode 100644 index 00000000..da6594ba --- /dev/null +++ b/2025/map/exception_handling.py @@ -0,0 +1,19 @@ +def parse_numbers(user_inputs: list[str]) -> list[int]: + valid_numbers: list[int] = [] + for value in user_inputs: + try: + number = int(value) + valid_numbers.append(number) + except ValueError: + print(f"Invalid input skipped: {value}") + return valid_numbers + + +def main() -> None: + user_inputs: list[str] = ["10", "20", "invalid", "30"] + valid_numbers = parse_numbers(user_inputs) + print("Valid numbers:", valid_numbers) + + +if __name__ == "__main__": + main() diff --git a/2025/map/exception_handling_map.py b/2025/map/exception_handling_map.py new file mode 100644 index 00000000..82bbcdf4 --- /dev/null +++ b/2025/map/exception_handling_map.py @@ -0,0 +1,19 @@ +def parse_numbers(user_inputs: list[str]) -> list[int]: + def safe_convert(value: str) -> int | None: + try: + return int(value) + except ValueError: + print(f"Invalid input skipped: {value}") + return None + + return list(filter(lambda x: x is not None, map(safe_convert, user_inputs))) + + +def main() -> None: + user_inputs: list[str] = ["10", "20", "invalid", "30"] + valid_numbers = parse_numbers(user_inputs) + print("Valid numbers:", valid_numbers) + + +if __name__ == "__main__": + main() diff --git a/2025/map/side_effects.py b/2025/map/side_effects.py new file mode 100644 index 00000000..fd22500f --- /dev/null +++ b/2025/map/side_effects.py @@ -0,0 +1,20 @@ +def write_warnings(log_entries: list[dict[str, str]]) -> None: + with open("warnings.txt", "w") as f: + for entry in log_entries: + timestamped = f"[{entry['timestamp']}] {entry['message']}" + f.write(timestamped + "\n") + if "WARNING" in entry["message"]: + print("WARNING:", timestamped) + + +def main() -> None: + log_entries: list[dict[str, str]] = [ + {"timestamp": "2025-04-01 12:00", "message": "INFO: System started"}, + {"timestamp": "2025-04-01 12:05", "message": "WARNING: Low disk space"}, + {"timestamp": "2025-04-01 12:10", "message": "ERROR: Disk failure"}, + ] + write_warnings(log_entries) + + +if __name__ == "__main__": + main() diff --git a/2025/map/side_effects_map.py b/2025/map/side_effects_map.py new file mode 100644 index 00000000..09b6666f --- /dev/null +++ b/2025/map/side_effects_map.py @@ -0,0 +1,22 @@ +def write_warnings(log_entries: list[dict[str, str]]) -> None: + def process(entry: dict[str, str]) -> None: + timestamped = f"[{entry['timestamp']}] {entry['message']}" + with open("warnings.txt", "a") as f: + f.write(timestamped + "\n") + if "WARNING" in entry["message"]: + print("WARNING:", timestamped) + + list(map(process, log_entries)) + + +def main() -> None: + log_entries: list[dict[str, str]] = [ + {"timestamp": "2025-04-01 12:00", "message": "INFO: System started"}, + {"timestamp": "2025-04-01 12:05", "message": "WARNING: Low disk space"}, + {"timestamp": "2025-04-01 12:10", "message": "ERROR: Disk failure"}, + ] + write_warnings(log_entries) + + +if __name__ == "__main__": + main() diff --git a/warnings.txt b/warnings.txt new file mode 100644 index 00000000..13d0c7d6 --- /dev/null +++ b/warnings.txt @@ -0,0 +1,3 @@ +[2023-10-01 12:00] INFO: System started +[2023-10-01 12:05] WARNING: Low disk space +[2023-10-01 12:10] ERROR: Disk failure From 94ba8dd75a1a6583ffd3540b54e4118ecfa05fe4 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 10 Apr 2025 17:21:46 +0200 Subject: [PATCH 007/113] Added first examples. --- 2025/anti/data_processing.py | 24 +++++ 2025/anti/exception_control_flow_after.py | 39 +++++++ 2025/anti/exception_control_flow_before.py | 40 ++++++++ 2025/anti/pyproject.toml | 8 ++ 2025/anti/static_methods_after.py | 23 +++++ 2025/anti/static_methods_before copy.py | 47 +++++++++ 2025/anti/uv.lock | 112 +++++++++++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 2025/anti/data_processing.py create mode 100644 2025/anti/exception_control_flow_after.py create mode 100644 2025/anti/exception_control_flow_before.py create mode 100644 2025/anti/pyproject.toml create mode 100644 2025/anti/static_methods_after.py create mode 100644 2025/anti/static_methods_before copy.py create mode 100644 2025/anti/uv.lock diff --git a/2025/anti/data_processing.py b/2025/anti/data_processing.py new file mode 100644 index 00000000..b69f5894 --- /dev/null +++ b/2025/anti/data_processing.py @@ -0,0 +1,24 @@ +import pandas as pd + + +def fill_missing_values(df: pd.DataFrame) -> pd.DataFrame: + """Fill missing numeric values with the median of each column.""" + return df.fillna(df.median(numeric_only=True)) + + +def normalize_columns(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame: + """Min-max normalize specified columns in the DataFrame.""" + df = df.copy() + for col in columns: + min_val = df[col].min() + max_val = df[col].max() + if min_val == max_val: + df[col] = 0.0 # avoid division by zero + else: + df[col] = (df[col] - min_val) / (max_val - min_val) + return df + + +def encode_categorical(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame: + """Convert categorical columns into one-hot encoded columns.""" + return pd.get_dummies(df, columns=columns, drop_first=True) diff --git a/2025/anti/exception_control_flow_after.py b/2025/anti/exception_control_flow_after.py new file mode 100644 index 00000000..89c4665c --- /dev/null +++ b/2025/anti/exception_control_flow_after.py @@ -0,0 +1,39 @@ +import random +import time + + +# Simulated API call that randomly succeeds or times out +def fetch_from_primary_api(city: str) -> dict[str, str]: + if simulate_timeout(): + return {"status": "error", "message": "Primary API timed out."} + return {"status": "success", "data": f"Weather in {city} is sunny."} + + +def fetch_from_backup_api(city: str) -> dict[str, str]: + if simulate_timeout(): + return {"status": "error", "message": "Backup API timed out."} + return {"status": "success", "data": f"Backup: Weather in {city} is cloudy."} + + +def simulate_timeout() -> bool: + time.sleep(0.2) # network delay + return random.random() < 0.5 + + +# Good: using return values for expected control flow +def get_weather_forecast(city: str) -> dict[str, str]: + result = fetch_from_primary_api(city) + if result["status"] == "error": + result = fetch_from_backup_api(city) + if result["status"] == "error": + return {"status": "error", "message": "Both APIs timed out."} + return result + + +def main(): + result = get_weather_forecast("New York") + print(result) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/exception_control_flow_before.py b/2025/anti/exception_control_flow_before.py new file mode 100644 index 00000000..cafce786 --- /dev/null +++ b/2025/anti/exception_control_flow_before.py @@ -0,0 +1,40 @@ +import random +import time + + +# Simulated API call that randomly succeeds or times out +def fetch_from_primary_api(city: str) -> dict[str, str]: + if simulate_timeout(): + raise TimeoutError("Primary API timed out.") + return {"status": "success", "data": f"Weather in {city} is sunny."} + + +def fetch_from_backup_api(city: str) -> dict[str, str]: + if simulate_timeout(): + raise TimeoutError("Backup API timed out.") + return {"status": "success", "data": f"Backup: Weather in {city} is cloudy."} + + +def simulate_timeout() -> bool: + time.sleep(0.2) # network delay + return random.random() < 0.5 + + +# Bad: using exceptions for expected control flow +def get_weather_forecast(city: str) -> dict[str, str]: + try: + return fetch_from_primary_api(city) + except TimeoutError: + try: + return fetch_from_backup_api(city) + except TimeoutError: + return {"status": "error", "message": "Both APIs timed out."} + + +def main(): + result = get_weather_forecast("New York") + print(result) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/pyproject.toml b/2025/anti/pyproject.toml new file mode 100644 index 00000000..d1f592a6 --- /dev/null +++ b/2025/anti/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "anti-patterns" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "numpy>=2.2.4", + "pandas>=2.2.3", +] diff --git a/2025/anti/static_methods_after.py b/2025/anti/static_methods_after.py new file mode 100644 index 00000000..15195d71 --- /dev/null +++ b/2025/anti/static_methods_after.py @@ -0,0 +1,23 @@ +import numpy as np +import pandas as pd +from data_processing import encode_categorical, fill_missing_values, normalize_columns + + +def main() -> None: + df = pd.DataFrame( + { + "age": [25, 30, np.nan, 22], + "income": [50000, 60000, 55000, np.nan], + "gender": ["male", "female", "female", "male"], + } + ) + + df = fill_missing_values(df) + df = normalize_columns(df, ["age", "income"]) + df = encode_categorical(df, ["gender"]) + + print(df) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/static_methods_before copy.py b/2025/anti/static_methods_before copy.py new file mode 100644 index 00000000..d49f8fbb --- /dev/null +++ b/2025/anti/static_methods_before copy.py @@ -0,0 +1,47 @@ +import numpy as np +import pandas as pd + + +class DataPreprocessingUtils: + @staticmethod + def fill_missing_values(df: pd.DataFrame) -> pd.DataFrame: + """Fill missing numeric values with the median of each column.""" + return df.fillna(df.median(numeric_only=True)) + + @staticmethod + def normalize_columns(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame: + """Min-max normalize specified columns in the DataFrame.""" + df = df.copy() + for col in columns: + min_val = df[col].min() + max_val = df[col].max() + if min_val == max_val: + df[col] = 0.0 # avoid division by zero + else: + df[col] = (df[col] - min_val) / (max_val - min_val) + return df + + @staticmethod + def encode_categorical(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame: + """Convert categorical columns into one-hot encoded columns.""" + return pd.get_dummies(df, columns=columns, drop_first=True) + + +def main() -> None: + df = pd.DataFrame( + { + "age": [25, 30, np.nan, 22], + "income": [50000, 60000, 55000, np.nan], + "gender": ["male", "female", "female", "male"], + } + ) + + df = DataPreprocessingUtils.fill_missing_values(df) + df = DataPreprocessingUtils.normalize_columns(df, ["age", "income"]) + df = DataPreprocessingUtils.encode_categorical(df, ["gender"]) + + print(df) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/uv.lock b/2025/anti/uv.lock new file mode 100644 index 00000000..c7be2814 --- /dev/null +++ b/2025/anti/uv.lock @@ -0,0 +1,112 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "anti-patterns" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2.2.4" }, + { name = "pandas", specifier = ">=2.2.3" }, +] + +[[package]] +name = "numpy" +version = "2.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 }, + { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 }, + { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 }, + { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 }, + { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 }, + { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 }, + { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 }, + { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 }, + { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 }, + { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 }, + { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 }, + { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 }, + { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 }, + { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 }, + { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 }, + { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 }, + { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 }, + { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 }, + { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 }, + { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] From 9c192b9b1e7e83019fd8bd84b6b3a48955542b41 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 17 Apr 2025 10:23:36 +0200 Subject: [PATCH 008/113] Added anti-pattern code examples. --- 2025/anti/abstraction.py | 98 ++++ 2025/anti/config.json | 5 + 2025/anti/decorator_inject_after.py | 17 + 2025/anti/decorator_inject_before.py | 30 ++ 2025/anti/hardcoded_after.py | 81 +++ 2025/anti/hardcoded_before.py | 39 ++ 2025/anti/inappropriate_intimacy.py | 66 +++ 2025/anti/overengineering_after.py | 59 +++ 2025/anti/overengineering_before.py | 69 +++ 2025/anti/overriding_new_after.py | 47 ++ 2025/anti/overriding_new_before.py | 30 ++ 2025/anti/pyproject.toml | 3 + ...efore copy.py => static_methods_before.py} | 0 2025/anti/uv.lock | 490 ++++++++++++++++++ 14 files changed, 1034 insertions(+) create mode 100644 2025/anti/abstraction.py create mode 100644 2025/anti/config.json create mode 100644 2025/anti/decorator_inject_after.py create mode 100644 2025/anti/decorator_inject_before.py create mode 100644 2025/anti/hardcoded_after.py create mode 100644 2025/anti/hardcoded_before.py create mode 100644 2025/anti/inappropriate_intimacy.py create mode 100644 2025/anti/overengineering_after.py create mode 100644 2025/anti/overengineering_before.py create mode 100644 2025/anti/overriding_new_after.py create mode 100644 2025/anti/overriding_new_before.py rename 2025/anti/{static_methods_before copy.py => static_methods_before.py} (100%) diff --git a/2025/anti/abstraction.py b/2025/anti/abstraction.py new file mode 100644 index 00000000..00e67365 --- /dev/null +++ b/2025/anti/abstraction.py @@ -0,0 +1,98 @@ +import json +from dataclasses import dataclass +from typing import Callable, Protocol + + +@dataclass +class Report: + title: str + content: str + + def to_csv(self) -> str: + return f"{self.title}, {self.content}\n" + + def to_json(self) -> str: + return json.dumps( + { + "title": self.title, + "content": self.content, + }, + indent=4, + ) + + +@dataclass +class Budget: + title: str + amount: float + + def to_csv(self) -> str: + return f"{self.title}, {self.amount}\n" + + def to_json(self) -> str: + return json.dumps( + { + "title": self.title, + "amount": self.amount, + }, + indent=4, + ) + + +type ExportFn = Callable[[str, Exportable], None] + + +class Exportable(Protocol): + def to_csv(self) -> str: ... + + def to_json(self) -> str: ... + + +def export_to_csv(filename: str, data: Exportable) -> None: + print("Exporting to CSV...") + with open(filename, "w") as f: + f.write(data.to_csv()) + print("Done.") + + +def export_to_json(filename: str, data: Exportable) -> None: + print("Exporting to JSON...") + with open(filename, "w") as f: + json.dump(data.to_json(), f) + print("Done.") + + +EXPORTERS = { + "csv": export_to_csv, + "json": export_to_json, +} + + +def get_exporter(format: str) -> ExportFn: + """Factory function to get the appropriate exporter function based on format string.""" + if format in EXPORTERS: + return EXPORTERS[format] + else: + raise ValueError(f"Unsupported export format: {format}") + + +def main() -> None: + report = Report( + title="Quarterly Earnings", + content="Here are the earnings for the last quarter...", + ) + + export_fn = get_exporter("json") + export_fn("report.json", report) + + budget = Budget( + title="Annual Budget", + amount=1000000.00, + ) + + export_fn = get_exporter("json") + export_fn("budget.json", budget) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/config.json b/2025/anti/config.json new file mode 100644 index 00000000..faff3b0d --- /dev/null +++ b/2025/anti/config.json @@ -0,0 +1,5 @@ +{ + "learning_rate": 0.01, + "batch_size": 32, + "epochs": 10 +} \ No newline at end of file diff --git a/2025/anti/decorator_inject_after.py b/2025/anti/decorator_inject_after.py new file mode 100644 index 00000000..cf4a453f --- /dev/null +++ b/2025/anti/decorator_inject_after.py @@ -0,0 +1,17 @@ +import json +from typing import Any + + +def load_config(file_name: str) -> dict[str, Any]: + """Load configuration from the specified file.""" + with open(file_name, "r") as f: + return json.load(f) + + +def main() -> None: + config = load_config("config.json") + print(f"Training with config: {config}") + + +if __name__ == "__main__": + main() diff --git a/2025/anti/decorator_inject_before.py b/2025/anti/decorator_inject_before.py new file mode 100644 index 00000000..787a7519 --- /dev/null +++ b/2025/anti/decorator_inject_before.py @@ -0,0 +1,30 @@ +import json +from functools import wraps +from typing import Any, Callable + + +def load_config(file_name: str) -> dict[str, Any]: + """Load configuration from the specified file.""" + with open(file_name, "r") as f: + return json.load(f) + + +def inject_config(file_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + config: dict[str, Any] = load_config(file_name) + return func(config, *args, **kwargs) + + return wrapper + + return decorator + + +@inject_config("config.json") +def main(config: dict[str, Any]) -> None: + print(f"Training with config: {config}") + + +if __name__ == "__main__": + main() diff --git a/2025/anti/hardcoded_after.py b/2025/anti/hardcoded_after.py new file mode 100644 index 00000000..5ad55ea6 --- /dev/null +++ b/2025/anti/hardcoded_after.py @@ -0,0 +1,81 @@ +import io +import json +import os +import zipfile + +import lokalise +import numpy as np +import pandas as pd +import requests +import streamlit as st +from dotenv import load_dotenv + +load_dotenv() +LOKALISE_API_KEY = os.getenv("LOKALISE_API_KEY") +LOKALISE_PROJECT_ID = os.getenv("LOKALISE_PROJECT_ID") +LANGUAGE = "nl" + + +class LokaliseTranslator: + def __init__(self, api_key: str, project_id: str, language: str) -> None: + self.client = lokalise.Client(api_key) + self.project_id = project_id + self.language = language + self.translations = self.get_translations() + + def get_translations(self) -> dict[str, str]: + response = self.client.download_files( + self.project_id, + {"format": "json", "original_filenames": True, "replace_breaks": False}, + ) + translations_url = response["bundle_url"] + + # Download and extract the ZIP file + zip_response = requests.get(translations_url) + zip_file = zipfile.ZipFile(io.BytesIO(zip_response.content)) + + # Find the JSON file corresponding to the selected language + json_filename = f"{self.language}/no_filename.json" + with zip_file.open(json_filename) as json_file: + return json.load(json_file) + + def __call__(self, key: str) -> str: + return self.translations.get(key, key) + + +translator = LokaliseTranslator(LOKALISE_API_KEY, LOKALISE_PROJECT_ID, LANGUAGE) + +st.title(translator("dashboard_title")) + +DATE_COLUMN = "date/time" +DATA_URL = ( + "https://s3-us-west-2.amazonaws.com/streamlit-demo-data/uber-raw-data-sep14.csv.gz" +) + + +@st.cache_data +def load_data(nrows): + data = pd.read_csv(DATA_URL, nrows=nrows) + data.rename(lambda x: str(x).lower(), axis="columns", inplace=True) + data[DATE_COLUMN] = pd.to_datetime(data[DATE_COLUMN]) + return data + + +data_load_state = st.text(translator("loading_data")) +data = load_data(10000) +data_load_state.text(translator("done")) + +if st.checkbox(translator("show_raw_data")): + st.subheader(translator("raw_data")) + st.write(data) + +st.subheader(translator("nb_pickups_hour")) +hist_values = np.histogram(data[DATE_COLUMN].dt.hour, bins=24, range=(0, 24))[0] +st.bar_chart(hist_values) + +# Some number in the range 0-23 +hour_to_filter = st.slider("hour", 0, 23, 17) +filtered_data = data[data[DATE_COLUMN].dt.hour == hour_to_filter] + +st.subheader(translator("map_all_pickups") % hour_to_filter) +st.map(filtered_data) diff --git a/2025/anti/hardcoded_before.py b/2025/anti/hardcoded_before.py new file mode 100644 index 00000000..7845eccb --- /dev/null +++ b/2025/anti/hardcoded_before.py @@ -0,0 +1,39 @@ +import numpy as np +import pandas as pd +import streamlit as st + +# Hardcoded constants +DATE_COLUMN = "date/time" +DATA_URL = ( + "https://s3-us-west-2.amazonaws.com/streamlit-demo-data/uber-raw-data-sep14.csv.gz" +) + + +@st.cache_data +def load_data(nrows: int) -> pd.DataFrame: + data = pd.read_csv(DATA_URL, nrows=nrows) + data.rename(lambda x: str(x).lower(), axis="columns", inplace=True) + data[DATE_COLUMN] = pd.to_datetime(data[DATE_COLUMN]) + return data + + +# ๐Ÿงจ All UI strings are hardcoded โ€” even small changes are tedious +st.title("Uber pickups in NYC") + +data_load_state = st.text("Loading data...") +data = load_data(10000) +data_load_state.text("Loading complete!") + +if st.checkbox("Show raw data"): + st.subheader("Raw data") + st.write(data) + +st.subheader("Number of pickups by hour") +hist_values = np.histogram(data[DATE_COLUMN].dt.hour, bins=24, range=(0, 24))[0] +st.bar_chart(hist_values) + +hour_to_filter = st.slider("Hour", 0, 23, 17) +filtered_data = data[data[DATE_COLUMN].dt.hour == hour_to_filter] + +st.subheader(f"Map of all pickups at {hour_to_filter}:00") +st.map(filtered_data) diff --git a/2025/anti/inappropriate_intimacy.py b/2025/anti/inappropriate_intimacy.py new file mode 100644 index 00000000..207bd1a1 --- /dev/null +++ b/2025/anti/inappropriate_intimacy.py @@ -0,0 +1,66 @@ +import json +from dataclasses import dataclass +from typing import Callable + + +@dataclass +class Report: + title: str + content: str + + def to_csv(self) -> str: + return f"{self.title}, {self.content}\n" + + def to_json(self) -> str: + return json.dumps( + { + "title": self.title, + "content": self.content, + }, + indent=4, + ) + + +type ExportFn = Callable[[str, Report], None] + + +def export_to_csv(filename: str, report: Report) -> None: + print("Exporting to CSV...") + with open(filename, "w") as f: + f.write(report.to_csv()) + print("Done.") + + +def export_to_json(filename: str, report: Report) -> None: + print("Exporting to JSON...") + with open(filename, "w") as f: + json.dump(report.to_json(), f) + print("Done.") + + +EXPORTERS = { + "csv": export_to_csv, + "json": export_to_json, +} + + +def get_exporter(format: str) -> ExportFn: + """Factory function to get the appropriate exporter function based on format string.""" + if format in EXPORTERS: + return EXPORTERS[format] + else: + raise ValueError(f"Unsupported export format: {format}") + + +def main() -> None: + report = Report( + title="Quarterly Earnings", + content="Here are the earnings for the last quarter...", + ) + + export_fn = get_exporter("json") + export_fn("report.json", report) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/overengineering_after.py b/2025/anti/overengineering_after.py new file mode 100644 index 00000000..4cfb7ee4 --- /dev/null +++ b/2025/anti/overengineering_after.py @@ -0,0 +1,59 @@ +import json +from dataclasses import dataclass +from typing import Callable + + +@dataclass +class Report: + title: str + content: str + + +type ExportFn = Callable[[str, Report], None] + + +def export_to_csv(filename: str, report: Report) -> None: + print("Exporting to CSV...") + csv_data = f"{report.title}, {report.content}\n" + with open(filename, "w") as f: + f.write(csv_data) + print("Done.") + + +def export_to_json(filename: str, report: Report) -> None: + print("Exporting to JSON...") + json_data = { + "title": report.title, + "content": report.content, + } + with open(filename, "w") as f: + json.dump(json_data, f) + print("Done.") + + +EXPORTERS = { + "csv": export_to_csv, + "json": export_to_json, +} + + +def get_exporter(format: str) -> ExportFn: + """Factory function to get the appropriate exporter function based on format string.""" + if format in EXPORTERS: + return EXPORTERS[format] + else: + raise ValueError(f"Unsupported export format: {format}") + + +def main() -> None: + report = Report( + title="Quarterly Earnings", + content="Here are the earnings for the last quarter...", + ) + + export_fn = get_exporter("json") + export_fn("report.json", report) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/overengineering_before.py b/2025/anti/overengineering_before.py new file mode 100644 index 00000000..5770be1d --- /dev/null +++ b/2025/anti/overengineering_before.py @@ -0,0 +1,69 @@ +import json +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass +class Report: + title: str + content: str + + +class Exporter(ABC): + """Abstract base class for data exporters.""" + + @abstractmethod + def export(self, filename: str, report: Report) -> None: + pass + + +class CSVExporter(Exporter): + """Exporter for CSV format.""" + + def export(self, filename: str, report: Report) -> None: + csv_data = f"{report.title}, {report.content}\n" + print("Exporting to CSV...") + with open(filename, "w") as f: + f.write(csv_data) + print("Done.") + + +class JSONExporter(Exporter): + """Exporter for JSON format.""" + + def export(self, filename: str, report: Report) -> None: + print("Exporting to JSON...") + json_data = { + "title": report.title, + "content": report.content, + } + with open(filename, "w") as f: + json.dump(json_data, f) + print("Done.") + + +class ExporterFactory: + """Factory for creating exporters based on format string.""" + + @staticmethod + def get_exporter(format: str) -> Exporter: + if format == "csv": + return CSVExporter() + elif format == "json": + return JSONExporter() + else: + raise ValueError(f"Unsupported export format: {format}") + + +def main() -> None: + report = Report( + title="Quarterly Earnings", + content="Here are the earnings for the last quarter...", + ) + + exporter = ExporterFactory.get_exporter("json") + exporter.export("report.json", report) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/overriding_new_after.py b/2025/anti/overriding_new_after.py new file mode 100644 index 00000000..b11e910f --- /dev/null +++ b/2025/anti/overriding_new_after.py @@ -0,0 +1,47 @@ +from enum import StrEnum +from typing import Callable + + +class PaymentMethod(StrEnum): + PAYPAL = "paypal" + CARD = "card" + + +type PaymentFn = Callable[[int], None] + + +def pay_paypal(amount: int) -> None: + print(f"Paying {amount} using Paypal") + + +def pay_stripe(amount: int) -> None: + print(f"Paying {amount} using Stripe") + + +def get_payment_method(payment_type: str) -> PaymentFn: + if payment_type == PaymentMethod.PAYPAL: + return pay_paypal + elif payment_type == PaymentMethod.CARD: + return pay_stripe + else: + raise ValueError(f"Unsupported payment type: {payment_type}") + + +PAYMENT_METHODS: dict[PaymentMethod, PaymentFn] = { + PaymentMethod.CARD: pay_stripe, + PaymentMethod.PAYPAL: pay_paypal, +} + + +def main(): + # using the dictionary + my_payment_fn = PAYMENT_METHODS[PaymentMethod.PAYPAL] + my_payment_fn(100) + + # using the function + my_payment_fn = get_payment_method("card") + my_payment_fn(100) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/overriding_new_before.py b/2025/anti/overriding_new_before.py new file mode 100644 index 00000000..e63ba6ba --- /dev/null +++ b/2025/anti/overriding_new_before.py @@ -0,0 +1,30 @@ +class Payment: + def __new__(cls, payment_type: str): + if payment_type == "paypal": + return object.__new__(PaypalPayment) + elif payment_type == "card": + return object.__new__(StripePayment) + else: + raise ValueError(f"Unsupported payment type: {payment_type}") + + def pay(self, amount: int) -> None: + raise NotImplementedError + + +class PaypalPayment(Payment): + def pay(self, amount: int) -> None: + print(f"Paying {amount} using Paypal") + + +class StripePayment(Payment): + def pay(self, amount: int) -> None: + print(f"Paying {amount} using Stripe") + + +def main() -> None: + my_payment = Payment("card") + my_payment.pay(100) + + +if __name__ == "__main__": + main() diff --git a/2025/anti/pyproject.toml b/2025/anti/pyproject.toml index d1f592a6..e53b2989 100644 --- a/2025/anti/pyproject.toml +++ b/2025/anti/pyproject.toml @@ -5,4 +5,7 @@ requires-python = ">=3.13" dependencies = [ "numpy>=2.2.4", "pandas>=2.2.3", + "python-dotenv>=1.1.0", + "python-lokalise-api>=3.2.0", + "streamlit>=1.44.1", ] diff --git a/2025/anti/static_methods_before copy.py b/2025/anti/static_methods_before.py similarity index 100% rename from 2025/anti/static_methods_before copy.py rename to 2025/anti/static_methods_before.py diff --git a/2025/anti/uv.lock b/2025/anti/uv.lock index c7be2814..76357cff 100644 --- a/2025/anti/uv.lock +++ b/2025/anti/uv.lock @@ -2,6 +2,22 @@ version = 1 revision = 1 requires-python = ">=3.13" +[[package]] +name = "altair" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, +] + [[package]] name = "anti-patterns" version = "0.1.0" @@ -9,12 +25,206 @@ source = { virtual = "." } dependencies = [ { name = "numpy" }, { name = "pandas" }, + { name = "python-dotenv" }, + { name = "python-lokalise-api" }, + { name = "streamlit" }, ] [package.metadata] requires-dist = [ { name = "numpy", specifier = ">=2.2.4" }, { name = "pandas", specifier = ">=2.2.3" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "python-lokalise-api", specifier = ">=3.2.0" }, + { name = "streamlit", specifier = ">=1.44.1" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "narwhals" +version = "1.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/6a/a98fa5e9d530a428a0cd79d27f059ed65efd3a07aad61a8c93e323c9c20b/narwhals-1.35.0.tar.gz", hash = "sha256:07477d18487fbc940243b69818a177ed7119b737910a8a254fb67688b48a7c96", size = 265784 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/b3/5781eb874f04cb1e882a7d93cf30abcb00362a3205c5f3708a7434a1a2ac/narwhals-1.35.0-py3-none-any.whl", hash = "sha256:7562af132fa3f8aaaf34dc96d7ec95bdca29d1c795e8fcf14e01edf1d32122bc", size = 325708 }, ] [[package]] @@ -45,6 +255,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 }, ] +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + [[package]] name = "pandas" version = "2.2.3" @@ -72,6 +291,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, ] +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, +] + +[[package]] +name = "protobuf" +version = "5.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/7d/b9dca7365f0e2c4fa7c193ff795427cfa6290147e5185ab11ece280a18e7/protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", size = 424902 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/b2/043a1a1a20edd134563699b0e91862726a0dc9146c090743b6c44d798e75/protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7", size = 422709 }, + { url = "https://files.pythonhosted.org/packages/79/fc/2474b59570daa818de6124c0a15741ee3e5d6302e9d6ce0bdfd12e98119f/protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d", size = 434506 }, + { url = "https://files.pythonhosted.org/packages/46/de/7c126bbb06aa0f8a7b38aaf8bd746c514d70e6a2a3f6dd460b3b7aad7aae/protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0", size = 417826 }, + { url = "https://files.pythonhosted.org/packages/a2/b5/bade14ae31ba871a139aa45e7a8183d869efe87c34a4850c87b936963261/protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", size = 319574 }, + { url = "https://files.pythonhosted.org/packages/46/88/b01ed2291aae68b708f7d334288ad5fb3e7aa769a9c309c91a0d55cb91b0/protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/12/fb/a586e0c973c95502e054ac5f81f88394f24ccc7982dac19c515acd9e2c93/protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", size = 172551 }, +] + +[[package]] +name = "pyarrow" +version = "19.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/09/a9046344212690f0632b9c709f9bf18506522feb333c894d0de81d62341a/pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e", size = 1129437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/8d/275c58d4b00781bd36579501a259eacc5c6dfb369be4ddeb672ceb551d2d/pyarrow-19.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e45274b20e524ae5c39d7fc1ca2aa923aab494776d2d4b316b49ec7572ca324c", size = 30653552 }, + { url = "https://files.pythonhosted.org/packages/a0/9e/e6aca5cc4ef0c7aec5f8db93feb0bde08dbad8c56b9014216205d271101b/pyarrow-19.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d9dedeaf19097a143ed6da37f04f4051aba353c95ef507764d344229b2b740ae", size = 32103413 }, + { url = "https://files.pythonhosted.org/packages/6a/fa/a7033f66e5d4f1308c7eb0dfcd2ccd70f881724eb6fd1776657fdf65458f/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebfb5171bb5f4a52319344ebbbecc731af3f021e49318c74f33d520d31ae0c4", size = 41134869 }, + { url = "https://files.pythonhosted.org/packages/2d/92/34d2569be8e7abdc9d145c98dc410db0071ac579b92ebc30da35f500d630/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a21d39fbdb948857f67eacb5bbaaf36802de044ec36fbef7a1c8f0dd3a4ab2", size = 42192626 }, + { url = "https://files.pythonhosted.org/packages/0a/1f/80c617b1084fc833804dc3309aa9d8daacd46f9ec8d736df733f15aebe2c/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:99bc1bec6d234359743b01e70d4310d0ab240c3d6b0da7e2a93663b0158616f6", size = 40496708 }, + { url = "https://files.pythonhosted.org/packages/e6/90/83698fcecf939a611c8d9a78e38e7fed7792dcc4317e29e72cf8135526fb/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1b93ef2c93e77c442c979b0d596af45e4665d8b96da598db145b0fec014b9136", size = 42075728 }, + { url = "https://files.pythonhosted.org/packages/40/49/2325f5c9e7a1c125c01ba0c509d400b152c972a47958768e4e35e04d13d8/pyarrow-19.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9d46e06846a41ba906ab25302cf0fd522f81aa2a85a71021826f34639ad31ef", size = 25242568 }, + { url = "https://files.pythonhosted.org/packages/3f/72/135088d995a759d4d916ec4824cb19e066585b4909ebad4ab196177aa825/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c0fe3dbbf054a00d1f162fda94ce236a899ca01123a798c561ba307ca38af5f0", size = 30702371 }, + { url = "https://files.pythonhosted.org/packages/2e/01/00beeebd33d6bac701f20816a29d2018eba463616bbc07397fdf99ac4ce3/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:96606c3ba57944d128e8a8399da4812f56c7f61de8c647e3470b417f795d0ef9", size = 32116046 }, + { url = "https://files.pythonhosted.org/packages/1f/c9/23b1ea718dfe967cbd986d16cf2a31fe59d015874258baae16d7ea0ccabc/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f04d49a6b64cf24719c080b3c2029a3a5b16417fd5fd7c4041f94233af732f3", size = 41091183 }, + { url = "https://files.pythonhosted.org/packages/3a/d4/b4a3aa781a2c715520aa8ab4fe2e7fa49d33a1d4e71c8fc6ab7b5de7a3f8/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9137cf7e1640dce4c190551ee69d478f7121b5c6f323553b319cac936395f6", size = 42171896 }, + { url = "https://files.pythonhosted.org/packages/23/1b/716d4cd5a3cbc387c6e6745d2704c4b46654ba2668260d25c402626c5ddb/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7c1bca1897c28013db5e4c83944a2ab53231f541b9e0c3f4791206d0c0de389a", size = 40464851 }, + { url = "https://files.pythonhosted.org/packages/ed/bd/54907846383dcc7ee28772d7e646f6c34276a17da740002a5cefe90f04f7/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:58d9397b2e273ef76264b45531e9d552d8ec8a6688b7390b5be44c02a37aade8", size = 42085744 }, +] + +[[package]] +name = "pydeck" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -84,6 +381,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "python-lokalise-api" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/38/bc1b7d47068b0de8ed115628ea9a5b097146521f0ef021782c3dacf8c06c/python_lokalise_api-3.2.0.tar.gz", hash = "sha256:e68b5df24aee49688ca4bfa3fcd47f5aff8cb6604bd0ab2fee7c384380aeb61f", size = 24647 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2e/4dbbb5e7a6148c5c81942e968154897e5289a99d293c716b78065e362c4c/python_lokalise_api-3.2.0-py3-none-any.whl", hash = "sha256:0227414b7e7fcff14b05a29ea3d612a99a117fc71d2452792df5903e3cc55404", size = 66771 }, +] + [[package]] name = "pytz" version = "2025.2" @@ -93,6 +411,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, +] + [[package]] name = "six" version = "1.17.0" @@ -102,6 +482,89 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "streamlit" +version = "1.44.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/c0/7286284567e5045f0c587c426d0c41aee5d10c0a2e360e627a83037e9f0c/streamlit-1.44.1.tar.gz", hash = "sha256:c6914ed6d5b76870b461510476806db370f36425ae0e6654d227c988288198d3", size = 9423685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/17/fc425e1d4d86e31b2aaf0812a2ef2163763a0670d671720c7c36e8679323/streamlit-1.44.1-py3-none-any.whl", hash = "sha256:9fe355f58b11f4eb71e74f115ce1f38c4c9eaff2733e6bcffb510ac1298a5990", size = 9812242 }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "tornado" +version = "6.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, + { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, + { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, + { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, + { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, + { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, + { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, + { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, + { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, + { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + [[package]] name = "tzdata" version = "2025.2" @@ -110,3 +573,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] From 21ef38915c431b1a70ad235960e3a9e08e3874b9 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 8 May 2025 15:57:15 +0200 Subject: [PATCH 009/113] Added initial abstraction example --- 2025/abstraction/input.jpg | Bin 0 -> 82063 bytes 2025/abstraction/main.py | 107 ++++++++++++++++++++++++++++++++ 2025/abstraction/pyproject.toml | 7 +++ 2025/abstraction/uv.lock | 44 +++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 2025/abstraction/input.jpg create mode 100644 2025/abstraction/main.py create mode 100644 2025/abstraction/pyproject.toml create mode 100644 2025/abstraction/uv.lock diff --git a/2025/abstraction/input.jpg b/2025/abstraction/input.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5b0fb06f958ab855530d234af9cd3d618fa1995f GIT binary patch literal 82063 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

qy%E8}tNN62)=p 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 literal 0 HcmV?d00001 diff --git a/2025/abstraction/main.py b/2025/abstraction/main.py new file mode 100644 index 00000000..ae1a3a93 --- /dev/null +++ b/2025/abstraction/main.py @@ -0,0 +1,107 @@ +from abc import ABC, abstractmethod +from typing import Callable, Protocol + +from PIL import Image, ImageOps + +# ----------- Callable-based abstraction ----------- + +# A Callable that takes an Image and returns an Image +ImageFilterFunc = Callable[[Image.Image], Image.Image] + + +def apply_grayscale(image: Image.Image) -> Image.Image: + print("Applying grayscale filter (function)...") + return ImageOps.grayscale(image) + + +def invert_filter(image: Image.Image) -> Image.Image: + print("Applying invert filter (function)...") + return ImageOps.invert(image.convert("RGB")) + + +def process_with_callable( + image_path: str, output_path: str, filter_func: ImageFilterFunc +): + image = Image.open(image_path) + image = filter_func(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +# ----------- ABC-based abstraction ----------- + + +class FilterBase(ABC): + @abstractmethod + def apply(self, image: Image.Image) -> Image.Image: ... + + +class GrayscaleFilter(FilterBase): + def apply(self, image: Image.Image) -> Image.Image: + print("Applying grayscale filter (class)...") + return ImageOps.grayscale(image) + + +class InvertFilter(FilterBase): + def apply(self, image: Image.Image) -> Image.Image: + print("Applying invert filter (class)...") + return ImageOps.invert(image.convert("RGB")) + + +def process_with_abc(image_path: str, output_path: str, filter_obj: FilterBase): + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +# ----------- Protocol-based abstraction ----------- + + +class ImageFilter(Protocol): + def apply(self, image: Image.Image) -> Image.Image: ... + + +# This class doesn't inherit anything but still conforms to the protocol +class SepiaFilter: + def apply(self, image: Image.Image) -> Image.Image: + print("Applying sepia filter (class conforming to protocol)...") + sepia_image = image.convert("RGB") + width, height = sepia_image.size + pixels = sepia_image.load() + + for y in range(height): + for x in range(width): + r, g, b = pixels[x, y] + tr = int(0.393 * r + 0.769 * g + 0.189 * b) + tg = int(0.349 * r + 0.686 * g + 0.168 * b) + tb = int(0.272 * r + 0.534 * g + 0.131 * b) + pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) + + return sepia_image + + +def process_with_protocol(image_path: str, output_path: str, filter_obj: ImageFilter): + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +def main() -> None: + input_image = "input.jpg" # Replace with your image path + + # Callable examples + process_with_callable(input_image, "output_callable_grayscale.jpg", apply_grayscale) + process_with_callable(input_image, "output_callable_invert.jpg", invert_filter) + + # ABC examples + process_with_abc(input_image, "output_abc_grayscale.jpg", GrayscaleFilter()) + process_with_abc(input_image, "output_abc_invert.jpg", InvertFilter()) + + # Protocol example + process_with_protocol(input_image, "output_protocol_sepia.jpg", SepiaFilter()) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/pyproject.toml b/2025/abstraction/pyproject.toml new file mode 100644 index 00000000..96b10630 --- /dev/null +++ b/2025/abstraction/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "abstraction" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "pillow>=11.2.1", +] diff --git a/2025/abstraction/uv.lock b/2025/abstraction/uv.lock new file mode 100644 index 00000000..a0335c3e --- /dev/null +++ b/2025/abstraction/uv.lock @@ -0,0 +1,44 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "abstraction" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pillow" }, +] + +[package.metadata] +requires-dist = [{ name = "pillow", specifier = ">=11.2.1" }] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] From fcbb868d52b29dcf20be203aa00865ad701beae1 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 8 May 2025 16:07:44 +0200 Subject: [PATCH 010/113] Updated examples. --- 2025/abstraction/abstraction_abc.py | 74 ++++++++++++++++ 2025/abstraction/abstraction_callable.py | 75 ++++++++++++++++ 2025/abstraction/abstraction_protocol.py | 65 ++++++++++++++ 2025/abstraction/main.py | 107 ----------------------- 4 files changed, 214 insertions(+), 107 deletions(-) create mode 100644 2025/abstraction/abstraction_abc.py create mode 100644 2025/abstraction/abstraction_callable.py create mode 100644 2025/abstraction/abstraction_protocol.py delete mode 100644 2025/abstraction/main.py diff --git a/2025/abstraction/abstraction_abc.py b/2025/abstraction/abstraction_abc.py new file mode 100644 index 00000000..0d45d7f7 --- /dev/null +++ b/2025/abstraction/abstraction_abc.py @@ -0,0 +1,74 @@ +from abc import ABC, abstractmethod +from typing import Any + +from PIL import Image, ImageOps + + +class FilterBase(ABC): + name: str + + @abstractmethod + def apply(self, image: Image.Image) -> Image.Image: ... + + @abstractmethod + def configure(self, config: dict[str, Any]) -> None: ... + + +class GrayscaleFilter(FilterBase): + def __init__(self) -> None: + self._intensity: float = 1.0 + + @property + def name(self) -> str: + return "Grayscale" + + def apply(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter with intensity {self._intensity}") + return ImageOps.grayscale(image) + + def configure(self, config: dict[str, Any]) -> None: + self._intensity = config.get("intensity", self._intensity) + + +class InvertFilter(FilterBase): + def __init__(self) -> None: + self._enabled: bool = True + + @property + def name(self) -> str: + return "Invert" + + def apply(self, image: Image.Image) -> Image.Image: + if self._enabled: + print(f"Applying {self.name} filter") + return ImageOps.invert(image.convert("RGB")) + else: + print(f"{self.name} filter disabled, returning original image") + return image + + def configure(self, config: dict[str, Any]) -> None: + self._enabled = config.get("enabled", self._enabled) + + +def process_with_abc(image_path: str, output_path: str, filter_obj: FilterBase) -> None: + print(f"\nUsing filter: {filter_obj.name}") + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +def main() -> None: + input_image: str = "input.jpg" + + grayscale = GrayscaleFilter() + grayscale.configure({"intensity": 0.8}) + process_with_abc(input_image, "output_abc_grayscale.jpg", grayscale) + + invert = InvertFilter() + invert.configure({"enabled": True}) + process_with_abc(input_image, "output_abc_invert.jpg", invert) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/abstraction_callable.py b/2025/abstraction/abstraction_callable.py new file mode 100644 index 00000000..c4d8e40f --- /dev/null +++ b/2025/abstraction/abstraction_callable.py @@ -0,0 +1,75 @@ +from typing import Callable + +from PIL import Image, ImageOps + +type ImageFilterFunc = Callable[[Image.Image], Image.Image] + + +def make_grayscale_filter(intensity: float = 1.0) -> ImageFilterFunc: + def filter_func(image: Image.Image) -> Image.Image: + print(f"Applying grayscale filter with intensity {intensity}") + # Note: intensity isn't actually used by Pillow here, but it demonstrates config + return ImageOps.grayscale(image) + + return filter_func + + +def make_invert_filter(enabled: bool = True) -> ImageFilterFunc: + def filter_func(image: Image.Image) -> Image.Image: + if enabled: + print("Applying invert filter") + return ImageOps.invert(image.convert("RGB")) + else: + print("Invert filter disabled, returning original image") + return image + + return filter_func + + +def make_sepia_filter(depth: int = 20) -> ImageFilterFunc: + def filter_func(image: Image.Image) -> Image.Image: + print(f"Applying sepia filter with depth {depth}") + sepia_image = image.convert("RGB") + width, height = sepia_image.size + pixels = sepia_image.load() + + for y in range(height): + for x in range(width): + r, g, b = pixels[x, y] + tr = int(0.393 * r + 0.769 * g + 0.189 * b + depth) + tg = int(0.349 * r + 0.686 * g + 0.168 * b + depth) + tb = int(0.272 * r + 0.534 * g + 0.131 * b + depth) + pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) + return sepia_image + + return filter_func + + +def process_image( + image_path: str, output_path: str, filter_func: ImageFilterFunc, filter_name: str +) -> None: + print(f"\nUsing filter: {filter_name}") + image = Image.open(image_path) + image = filter_func(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +def main() -> None: + input_image: str = "input.jpg" + + # Create configured filters + grayscale_filter = make_grayscale_filter(intensity=0.8) + invert_filter = make_invert_filter(enabled=True) + sepia_filter = make_sepia_filter(depth=15) + + # Apply filters + process_image( + input_image, "output_callable_grayscale.jpg", grayscale_filter, "Grayscale" + ) + process_image(input_image, "output_callable_invert.jpg", invert_filter, "Invert") + process_image(input_image, "output_callable_sepia.jpg", sepia_filter, "Sepia") + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/abstraction_protocol.py b/2025/abstraction/abstraction_protocol.py new file mode 100644 index 00000000..849b2b7d --- /dev/null +++ b/2025/abstraction/abstraction_protocol.py @@ -0,0 +1,65 @@ +from typing import Any, Protocol + +from PIL import Image + + +class ImageFilter(Protocol): + name: str + + def apply(self, image: Image.Image) -> Image.Image: ... + + def configure(self, config: dict[str, Any]) -> None: ... + + +class SepiaFilter: + def __init__(self) -> None: + self._depth: int = 20 + + @property + def name(self) -> str: + return "Sepia" + + def apply(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter with depth {self._depth}") + sepia_image = image.convert("RGB") + width, height = sepia_image.size + pixels = sepia_image.load() + + for y in range(height): + for x in range(width): + r, g, b = pixels[x, y] + tr = int(0.393 * r + 0.769 * g + 0.189 * b + self._depth) + tg = int(0.349 * r + 0.686 * g + 0.168 * b + self._depth) + tb = int(0.272 * r + 0.534 * g + 0.131 * b + self._depth) + pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) + + return sepia_image + + def configure(self, config: dict[str, Any]) -> None: + self._depth = config.get("depth", self._depth) + + +def process_with_protocol( + image_path: str, output_path: str, filter_obj: ImageFilter +) -> None: + print(f"\nUsing filter: {filter_obj.name}") + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") + + +# ----------- Main function ----------- + + +def main() -> None: + input_image: str = "input.jpg" # Replace with a real image path + + # Protocol example + sepia = SepiaFilter() + sepia.configure({"depth": 15}) + process_with_protocol(input_image, "output_protocol_sepia.jpg", sepia) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/main.py b/2025/abstraction/main.py deleted file mode 100644 index ae1a3a93..00000000 --- a/2025/abstraction/main.py +++ /dev/null @@ -1,107 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Callable, Protocol - -from PIL import Image, ImageOps - -# ----------- Callable-based abstraction ----------- - -# A Callable that takes an Image and returns an Image -ImageFilterFunc = Callable[[Image.Image], Image.Image] - - -def apply_grayscale(image: Image.Image) -> Image.Image: - print("Applying grayscale filter (function)...") - return ImageOps.grayscale(image) - - -def invert_filter(image: Image.Image) -> Image.Image: - print("Applying invert filter (function)...") - return ImageOps.invert(image.convert("RGB")) - - -def process_with_callable( - image_path: str, output_path: str, filter_func: ImageFilterFunc -): - image = Image.open(image_path) - image = filter_func(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -# ----------- ABC-based abstraction ----------- - - -class FilterBase(ABC): - @abstractmethod - def apply(self, image: Image.Image) -> Image.Image: ... - - -class GrayscaleFilter(FilterBase): - def apply(self, image: Image.Image) -> Image.Image: - print("Applying grayscale filter (class)...") - return ImageOps.grayscale(image) - - -class InvertFilter(FilterBase): - def apply(self, image: Image.Image) -> Image.Image: - print("Applying invert filter (class)...") - return ImageOps.invert(image.convert("RGB")) - - -def process_with_abc(image_path: str, output_path: str, filter_obj: FilterBase): - image = Image.open(image_path) - image = filter_obj.apply(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -# ----------- Protocol-based abstraction ----------- - - -class ImageFilter(Protocol): - def apply(self, image: Image.Image) -> Image.Image: ... - - -# This class doesn't inherit anything but still conforms to the protocol -class SepiaFilter: - def apply(self, image: Image.Image) -> Image.Image: - print("Applying sepia filter (class conforming to protocol)...") - sepia_image = image.convert("RGB") - width, height = sepia_image.size - pixels = sepia_image.load() - - for y in range(height): - for x in range(width): - r, g, b = pixels[x, y] - tr = int(0.393 * r + 0.769 * g + 0.189 * b) - tg = int(0.349 * r + 0.686 * g + 0.168 * b) - tb = int(0.272 * r + 0.534 * g + 0.131 * b) - pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) - - return sepia_image - - -def process_with_protocol(image_path: str, output_path: str, filter_obj: ImageFilter): - image = Image.open(image_path) - image = filter_obj.apply(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -def main() -> None: - input_image = "input.jpg" # Replace with your image path - - # Callable examples - process_with_callable(input_image, "output_callable_grayscale.jpg", apply_grayscale) - process_with_callable(input_image, "output_callable_invert.jpg", invert_filter) - - # ABC examples - process_with_abc(input_image, "output_abc_grayscale.jpg", GrayscaleFilter()) - process_with_abc(input_image, "output_abc_invert.jpg", InvertFilter()) - - # Protocol example - process_with_protocol(input_image, "output_protocol_sepia.jpg", SepiaFilter()) - - -if __name__ == "__main__": - main() From 781137259a1a3d4357c810b466041deb855fa6f3 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 8 May 2025 16:10:13 +0200 Subject: [PATCH 011/113] Some cleanup. --- 2025/abstraction/abstraction_abc.py | 4 +++- 2025/abstraction/abstraction_protocol.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/2025/abstraction/abstraction_abc.py b/2025/abstraction/abstraction_abc.py index 0d45d7f7..7c63cd7e 100644 --- a/2025/abstraction/abstraction_abc.py +++ b/2025/abstraction/abstraction_abc.py @@ -5,7 +5,9 @@ class FilterBase(ABC): - name: str + @property + @abstractmethod + def name(self) -> str: ... @abstractmethod def apply(self, image: Image.Image) -> Image.Image: ... diff --git a/2025/abstraction/abstraction_protocol.py b/2025/abstraction/abstraction_protocol.py index 849b2b7d..59027783 100644 --- a/2025/abstraction/abstraction_protocol.py +++ b/2025/abstraction/abstraction_protocol.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Any, Protocol from PIL import Image @@ -11,13 +12,10 @@ def apply(self, image: Image.Image) -> Image.Image: ... def configure(self, config: dict[str, Any]) -> None: ... +@dataclass class SepiaFilter: - def __init__(self) -> None: - self._depth: int = 20 - - @property - def name(self) -> str: - return "Sepia" + name: str = "Sepia" + _depth: int = 20 def apply(self, image: Image.Image) -> Image.Image: print(f"Applying {self.name} filter with depth {self._depth}") From ce84514d3e90e9498e384f3ae8e216bd0574c169 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 12 May 2025 17:23:43 +0200 Subject: [PATCH 012/113] WiP on abstraction example --- 2025/abstraction/abstraction_abc.py | 76 ------------------ .../abstraction_abc/filters/base.py | 14 ++++ .../abstraction_abc/filters/grayscale.py | 18 +++++ .../abstraction_abc/filters/invert.py | 22 +++++ 2025/abstraction/abstraction_abc/main.py | 20 +++++ .../abstraction_abc/output_abc_grayscale.jpg | Bin 0 -> 26854 bytes .../abstraction_abc/output_abc_invert.jpg | Bin 0 -> 30626 bytes .../abstraction_abc/process_img.py | 9 +++ 2025/abstraction/abstraction_none/main.py | 18 +++++ 2025/abstraction/filters/grayscale.py | 6 ++ 2025/abstraction/filters/invert.py | 6 ++ 2025/abstraction/filters/sepia.py | 18 +++++ 12 files changed, 131 insertions(+), 76 deletions(-) delete mode 100644 2025/abstraction/abstraction_abc.py create mode 100644 2025/abstraction/abstraction_abc/filters/base.py create mode 100644 2025/abstraction/abstraction_abc/filters/grayscale.py create mode 100644 2025/abstraction/abstraction_abc/filters/invert.py create mode 100644 2025/abstraction/abstraction_abc/main.py create mode 100644 2025/abstraction/abstraction_abc/output_abc_grayscale.jpg create mode 100644 2025/abstraction/abstraction_abc/output_abc_invert.jpg create mode 100644 2025/abstraction/abstraction_abc/process_img.py create mode 100644 2025/abstraction/abstraction_none/main.py create mode 100644 2025/abstraction/filters/grayscale.py create mode 100644 2025/abstraction/filters/invert.py create mode 100644 2025/abstraction/filters/sepia.py diff --git a/2025/abstraction/abstraction_abc.py b/2025/abstraction/abstraction_abc.py deleted file mode 100644 index 7c63cd7e..00000000 --- a/2025/abstraction/abstraction_abc.py +++ /dev/null @@ -1,76 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any - -from PIL import Image, ImageOps - - -class FilterBase(ABC): - @property - @abstractmethod - def name(self) -> str: ... - - @abstractmethod - def apply(self, image: Image.Image) -> Image.Image: ... - - @abstractmethod - def configure(self, config: dict[str, Any]) -> None: ... - - -class GrayscaleFilter(FilterBase): - def __init__(self) -> None: - self._intensity: float = 1.0 - - @property - def name(self) -> str: - return "Grayscale" - - def apply(self, image: Image.Image) -> Image.Image: - print(f"Applying {self.name} filter with intensity {self._intensity}") - return ImageOps.grayscale(image) - - def configure(self, config: dict[str, Any]) -> None: - self._intensity = config.get("intensity", self._intensity) - - -class InvertFilter(FilterBase): - def __init__(self) -> None: - self._enabled: bool = True - - @property - def name(self) -> str: - return "Invert" - - def apply(self, image: Image.Image) -> Image.Image: - if self._enabled: - print(f"Applying {self.name} filter") - return ImageOps.invert(image.convert("RGB")) - else: - print(f"{self.name} filter disabled, returning original image") - return image - - def configure(self, config: dict[str, Any]) -> None: - self._enabled = config.get("enabled", self._enabled) - - -def process_with_abc(image_path: str, output_path: str, filter_obj: FilterBase) -> None: - print(f"\nUsing filter: {filter_obj.name}") - image = Image.open(image_path) - image = filter_obj.apply(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -def main() -> None: - input_image: str = "input.jpg" - - grayscale = GrayscaleFilter() - grayscale.configure({"intensity": 0.8}) - process_with_abc(input_image, "output_abc_grayscale.jpg", grayscale) - - invert = InvertFilter() - invert.configure({"enabled": True}) - process_with_abc(input_image, "output_abc_invert.jpg", invert) - - -if __name__ == "__main__": - main() diff --git a/2025/abstraction/abstraction_abc/filters/base.py b/2025/abstraction/abstraction_abc/filters/base.py new file mode 100644 index 00000000..f5472d15 --- /dev/null +++ b/2025/abstraction/abstraction_abc/filters/base.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod +from PIL import Image +from typing import Any + +class FilterBase(ABC): + @property + @abstractmethod + def name(self) -> str: ... + + @abstractmethod + def apply(self, image: Image.Image) -> Image.Image: ... + + @abstractmethod + def configure(self, config: dict[str, Any]) -> None: ... \ No newline at end of file diff --git a/2025/abstraction/abstraction_abc/filters/grayscale.py b/2025/abstraction/abstraction_abc/filters/grayscale.py new file mode 100644 index 00000000..25d8503d --- /dev/null +++ b/2025/abstraction/abstraction_abc/filters/grayscale.py @@ -0,0 +1,18 @@ +from .base import FilterBase +from PIL import Image, ImageOps +from typing import Any + +class GrayscaleFilter(FilterBase): + def __init__(self) -> None: + self._intensity: float = 1.0 + + @property + def name(self) -> str: + return "Grayscale" + + def apply(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter with intensity {self._intensity}") + return ImageOps.grayscale(image) + + def configure(self, config: dict[str, Any]) -> None: + self._intensity = config.get("intensity", self._intensity) \ No newline at end of file diff --git a/2025/abstraction/abstraction_abc/filters/invert.py b/2025/abstraction/abstraction_abc/filters/invert.py new file mode 100644 index 00000000..3c65cfd0 --- /dev/null +++ b/2025/abstraction/abstraction_abc/filters/invert.py @@ -0,0 +1,22 @@ +from .base import FilterBase +from PIL import Image, ImageOps +from typing import Any + +class InvertFilter(FilterBase): + def __init__(self) -> None: + self._enabled: bool = True + + @property + def name(self) -> str: + return "Invert" + + def apply(self, image: Image.Image) -> Image.Image: + if self._enabled: + print(f"Applying {self.name} filter") + return ImageOps.invert(image.convert("RGB")) + else: + print(f"{self.name} filter disabled, returning original image") + return image + + def configure(self, config: dict[str, Any]) -> None: + self._enabled = config.get("enabled", self._enabled) \ No newline at end of file diff --git a/2025/abstraction/abstraction_abc/main.py b/2025/abstraction/abstraction_abc/main.py new file mode 100644 index 00000000..11051f4f --- /dev/null +++ b/2025/abstraction/abstraction_abc/main.py @@ -0,0 +1,20 @@ +from typing import Any +from process_img import process_with_abc +from filters.grayscale import GrayscaleFilter +from filters.invert import InvertFilter + + +def main() -> None: + input_image: str = "../input.jpg" + + grayscale = GrayscaleFilter() + grayscale.configure({"intensity": 0.8}) + process_with_abc(input_image, "output_abc_grayscale.jpg", grayscale) + + invert = InvertFilter() + invert.configure({"enabled": True}) + process_with_abc(input_image, "output_abc_invert.jpg", invert) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/abstraction_abc/output_abc_grayscale.jpg b/2025/abstraction/abstraction_abc/output_abc_grayscale.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a7c4c10efdea8fe310e6456f2c74c2fc0f291642 GIT binary patch literal 26854 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 S`cu>nwPAkkKNN%yO8?ocrJ#QR literal 0 HcmV?d00001 diff --git a/2025/abstraction/abstraction_abc/output_abc_invert.jpg b/2025/abstraction/abstraction_abc/output_abc_invert.jpg new file mode 100644 index 0000000000000000000000000000000000000000..61f726b701c688fe951dbc70ea8e7888fb586384 GIT binary patch literal 30626 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 None: + print(f"\nUsing filter: {filter_obj.name}") + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") \ No newline at end of file diff --git a/2025/abstraction/abstraction_none/main.py b/2025/abstraction/abstraction_none/main.py new file mode 100644 index 00000000..2645b5a1 --- /dev/null +++ b/2025/abstraction/abstraction_none/main.py @@ -0,0 +1,18 @@ +from PIL import Image +from filters.grayscale import apply_grayscale +from filters.invert import apply_invert +from filters.sepia import apply_sepia + + +def main() -> None: + image = Image.open("input.jpg") + image = apply_grayscale(image) + image = apply_invert(image) + image = apply_sepia(image) + + image.save("output.jpg") + print("Saved output.jpg") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/abstraction/filters/grayscale.py b/2025/abstraction/filters/grayscale.py new file mode 100644 index 00000000..bc3dcd63 --- /dev/null +++ b/2025/abstraction/filters/grayscale.py @@ -0,0 +1,6 @@ +from PIL import Image, ImageOps + + +def apply_grayscale(image: Image.Image) -> Image.Image: + print("Applying grayscale filter") + return ImageOps.grayscale(image) \ No newline at end of file diff --git a/2025/abstraction/filters/invert.py b/2025/abstraction/filters/invert.py new file mode 100644 index 00000000..da0240fd --- /dev/null +++ b/2025/abstraction/filters/invert.py @@ -0,0 +1,6 @@ +from PIL import Image, ImageOps + + +def apply_invert(image: Image.Image) -> Image.Image: + print("Applying invert filter") + return ImageOps.invert(image.convert("RGB")) \ No newline at end of file diff --git a/2025/abstraction/filters/sepia.py b/2025/abstraction/filters/sepia.py new file mode 100644 index 00000000..d0c2bd31 --- /dev/null +++ b/2025/abstraction/filters/sepia.py @@ -0,0 +1,18 @@ +from PIL import Image + + +def apply_sepia(image: Image.Image) -> Image.Image: + print("Applying sepia filter") + sepia_image = image.convert("RGB") + width, height = sepia_image.size + pixels = sepia_image.load() + + for y in range(height): + for x in range(width): + r, g, b = pixels[x, y] + tr = int(0.393 * r + 0.769 * g + 0.189 * b) + tg = int(0.349 * r + 0.686 * g + 0.168 * b) + tb = int(0.272 * r + 0.534 * g + 0.131 * b) + pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) + + return sepia_image \ No newline at end of file From 714fda4638729f3c0b0b3accf1ac7940e8b54dce Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 14 May 2025 15:58:07 +0200 Subject: [PATCH 013/113] Removed output images. Cleaned up examples and organized them around the various types of abstraction. --- .../abstraction_abc/filters/base.py | 6 +- .../abstraction_abc/filters/grayscale.py | 14 +++- 2025/abstraction/abstraction_abc/main.py | 7 +- .../abstraction_abc/output_abc_grayscale.jpg | Bin 26854 -> 0 bytes .../abstraction_abc/output_abc_invert.jpg | Bin 30626 -> 0 bytes .../abstraction_abc/process_img.py | 7 +- 2025/abstraction/abstraction_callable.py | 75 ------------------ .../abstraction_callable/filters/grayscale.py | 9 +++ .../filters/invert.py | 0 .../filters/sepia.py | 0 2025/abstraction/abstraction_callable/main.py | 40 ++++++++++ .../abstraction_callable/process_img.py | 13 +++ .../abstraction_none/filters/grayscale.py | 22 +++++ .../abstraction_none/filters/invert.py | 11 +++ 2025/abstraction/abstraction_none/main.py | 22 ++--- .../abstraction_none/process_img.py | 20 +++++ 2025/abstraction/abstraction_protocol.py | 63 --------------- .../abstraction_protocol/filters/grayscale.py | 22 +++++ .../abstraction_protocol/filters/invert.py | 23 ++++++ 2025/abstraction/abstraction_protocol/main.py | 18 +++++ .../abstraction_protocol/process_img.py | 20 +++++ 2025/abstraction/filters/grayscale.py | 6 -- 22 files changed, 230 insertions(+), 168 deletions(-) delete mode 100644 2025/abstraction/abstraction_abc/output_abc_grayscale.jpg delete mode 100644 2025/abstraction/abstraction_abc/output_abc_invert.jpg delete mode 100644 2025/abstraction/abstraction_callable.py create mode 100644 2025/abstraction/abstraction_callable/filters/grayscale.py rename 2025/abstraction/{ => abstraction_callable}/filters/invert.py (100%) rename 2025/abstraction/{ => abstraction_callable}/filters/sepia.py (100%) create mode 100644 2025/abstraction/abstraction_callable/main.py create mode 100644 2025/abstraction/abstraction_callable/process_img.py create mode 100644 2025/abstraction/abstraction_none/filters/grayscale.py create mode 100644 2025/abstraction/abstraction_none/filters/invert.py create mode 100644 2025/abstraction/abstraction_none/process_img.py delete mode 100644 2025/abstraction/abstraction_protocol.py create mode 100644 2025/abstraction/abstraction_protocol/filters/grayscale.py create mode 100644 2025/abstraction/abstraction_protocol/filters/invert.py create mode 100644 2025/abstraction/abstraction_protocol/main.py create mode 100644 2025/abstraction/abstraction_protocol/process_img.py delete mode 100644 2025/abstraction/filters/grayscale.py diff --git a/2025/abstraction/abstraction_abc/filters/base.py b/2025/abstraction/abstraction_abc/filters/base.py index f5472d15..c1e9a964 100644 --- a/2025/abstraction/abstraction_abc/filters/base.py +++ b/2025/abstraction/abstraction_abc/filters/base.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod -from PIL import Image from typing import Any +from PIL import Image + + class FilterBase(ABC): @property @abstractmethod @@ -11,4 +13,4 @@ def name(self) -> str: ... def apply(self, image: Image.Image) -> Image.Image: ... @abstractmethod - def configure(self, config: dict[str, Any]) -> None: ... \ No newline at end of file + def configure(self, config: dict[str, Any]) -> None: ... diff --git a/2025/abstraction/abstraction_abc/filters/grayscale.py b/2025/abstraction/abstraction_abc/filters/grayscale.py index 25d8503d..6e7ecdd3 100644 --- a/2025/abstraction/abstraction_abc/filters/grayscale.py +++ b/2025/abstraction/abstraction_abc/filters/grayscale.py @@ -1,7 +1,10 @@ -from .base import FilterBase -from PIL import Image, ImageOps from typing import Any +from PIL import Image, ImageOps + +from .base import FilterBase + + class GrayscaleFilter(FilterBase): def __init__(self) -> None: self._intensity: float = 1.0 @@ -12,7 +15,10 @@ def name(self) -> str: def apply(self, image: Image.Image) -> Image.Image: print(f"Applying {self.name} filter with intensity {self._intensity}") - return ImageOps.grayscale(image) + grayscale_image = ImageOps.grayscale(image) + if self._intensity < 1.0: + return Image.blend(image, grayscale_image, self._intensity) + return grayscale_image def configure(self, config: dict[str, Any]) -> None: - self._intensity = config.get("intensity", self._intensity) \ No newline at end of file + self._intensity = config.get("intensity", self._intensity) diff --git a/2025/abstraction/abstraction_abc/main.py b/2025/abstraction/abstraction_abc/main.py index 11051f4f..ef18c424 100644 --- a/2025/abstraction/abstraction_abc/main.py +++ b/2025/abstraction/abstraction_abc/main.py @@ -1,7 +1,6 @@ -from typing import Any -from process_img import process_with_abc from filters.grayscale import GrayscaleFilter from filters.invert import InvertFilter +from process_img import process_img def main() -> None: @@ -9,11 +8,11 @@ def main() -> None: grayscale = GrayscaleFilter() grayscale.configure({"intensity": 0.8}) - process_with_abc(input_image, "output_abc_grayscale.jpg", grayscale) + process_img(input_image, "output_abc_grayscale.jpg", grayscale) invert = InvertFilter() invert.configure({"enabled": True}) - process_with_abc(input_image, "output_abc_invert.jpg", invert) + process_img(input_image, "output_abc_invert.jpg", invert) if __name__ == "__main__": diff --git a/2025/abstraction/abstraction_abc/output_abc_grayscale.jpg b/2025/abstraction/abstraction_abc/output_abc_grayscale.jpg deleted file mode 100644 index a7c4c10efdea8fe310e6456f2c74c2fc0f291642..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26854 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 S`cu>nwPAkkKNN%yO8?ocrJ#QR diff --git a/2025/abstraction/abstraction_abc/output_abc_invert.jpg b/2025/abstraction/abstraction_abc/output_abc_invert.jpg deleted file mode 100644 index 61f726b701c688fe951dbc70ea8e7888fb586384..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30626 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 None: +def process_img(image_path: str, output_path: str, filter_obj: FilterBase) -> None: print(f"\nUsing filter: {filter_obj.name}") image = Image.open(image_path) image = filter_obj.apply(image) image.save(output_path) - print(f"Saved processed image to {output_path}") \ No newline at end of file + print(f"Saved processed image to {output_path}") diff --git a/2025/abstraction/abstraction_callable.py b/2025/abstraction/abstraction_callable.py deleted file mode 100644 index c4d8e40f..00000000 --- a/2025/abstraction/abstraction_callable.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import Callable - -from PIL import Image, ImageOps - -type ImageFilterFunc = Callable[[Image.Image], Image.Image] - - -def make_grayscale_filter(intensity: float = 1.0) -> ImageFilterFunc: - def filter_func(image: Image.Image) -> Image.Image: - print(f"Applying grayscale filter with intensity {intensity}") - # Note: intensity isn't actually used by Pillow here, but it demonstrates config - return ImageOps.grayscale(image) - - return filter_func - - -def make_invert_filter(enabled: bool = True) -> ImageFilterFunc: - def filter_func(image: Image.Image) -> Image.Image: - if enabled: - print("Applying invert filter") - return ImageOps.invert(image.convert("RGB")) - else: - print("Invert filter disabled, returning original image") - return image - - return filter_func - - -def make_sepia_filter(depth: int = 20) -> ImageFilterFunc: - def filter_func(image: Image.Image) -> Image.Image: - print(f"Applying sepia filter with depth {depth}") - sepia_image = image.convert("RGB") - width, height = sepia_image.size - pixels = sepia_image.load() - - for y in range(height): - for x in range(width): - r, g, b = pixels[x, y] - tr = int(0.393 * r + 0.769 * g + 0.189 * b + depth) - tg = int(0.349 * r + 0.686 * g + 0.168 * b + depth) - tb = int(0.272 * r + 0.534 * g + 0.131 * b + depth) - pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) - return sepia_image - - return filter_func - - -def process_image( - image_path: str, output_path: str, filter_func: ImageFilterFunc, filter_name: str -) -> None: - print(f"\nUsing filter: {filter_name}") - image = Image.open(image_path) - image = filter_func(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -def main() -> None: - input_image: str = "input.jpg" - - # Create configured filters - grayscale_filter = make_grayscale_filter(intensity=0.8) - invert_filter = make_invert_filter(enabled=True) - sepia_filter = make_sepia_filter(depth=15) - - # Apply filters - process_image( - input_image, "output_callable_grayscale.jpg", grayscale_filter, "Grayscale" - ) - process_image(input_image, "output_callable_invert.jpg", invert_filter, "Invert") - process_image(input_image, "output_callable_sepia.jpg", sepia_filter, "Sepia") - - -if __name__ == "__main__": - main() diff --git a/2025/abstraction/abstraction_callable/filters/grayscale.py b/2025/abstraction/abstraction_callable/filters/grayscale.py new file mode 100644 index 00000000..bdc0bf96 --- /dev/null +++ b/2025/abstraction/abstraction_callable/filters/grayscale.py @@ -0,0 +1,9 @@ +from PIL import Image, ImageOps + + +def apply_grayscale(image: Image.Image, intensity: float) -> Image.Image: + print(f"Applying grayscale filter with intensity {intensity}") + grayscale_image = ImageOps.grayscale(image) + if intensity < 1.0: + return Image.blend(image, grayscale_image, intensity) + return grayscale_image diff --git a/2025/abstraction/filters/invert.py b/2025/abstraction/abstraction_callable/filters/invert.py similarity index 100% rename from 2025/abstraction/filters/invert.py rename to 2025/abstraction/abstraction_callable/filters/invert.py diff --git a/2025/abstraction/filters/sepia.py b/2025/abstraction/abstraction_callable/filters/sepia.py similarity index 100% rename from 2025/abstraction/filters/sepia.py rename to 2025/abstraction/abstraction_callable/filters/sepia.py diff --git a/2025/abstraction/abstraction_callable/main.py b/2025/abstraction/abstraction_callable/main.py new file mode 100644 index 00000000..b0b92af7 --- /dev/null +++ b/2025/abstraction/abstraction_callable/main.py @@ -0,0 +1,40 @@ +from functools import partial + +from filters.grayscale import apply_grayscale +from filters.invert import apply_invert +from PIL import Image, ImageOps +from process_img import process_img + + +def make_grayscale_filter(intensity: float = 1.0) -> ImageFilterFunc: + def filter_func(image: Image.Image) -> Image.Image: + print(f"Applying grayscale filter with intensity {intensity}") + # Note: intensity isn't actually used by Pillow here, but it demonstrates config + return ImageOps.grayscale(image) + + return filter_func + + +def make_invert_filter(enabled: bool = True) -> ImageFilterFunc: + def filter_func(image: Image.Image) -> Image.Image: + if enabled: + print("Applying invert filter") + return ImageOps.invert(image.convert("RGB")) + else: + print("Invert filter disabled, returning original image") + return image + + return filter_func + + +def main() -> None: + input_image: str = "../input.jpg" + + grayscale_fn = partial(apply_grayscale, intensity=0.8) + process_img(input_image, "output_abc_grayscale.jpg", grayscale_fn) + + process_img(input_image, "output_abc_invert.jpg", apply_invert) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/abstraction_callable/process_img.py b/2025/abstraction/abstraction_callable/process_img.py new file mode 100644 index 00000000..946fd09c --- /dev/null +++ b/2025/abstraction/abstraction_callable/process_img.py @@ -0,0 +1,13 @@ +from typing import Callable + +from PIL import Image + +type ProcessFn = Callable[[image.Image], image.Image] + + +def process_img(image_path: str, output_path: str, filter_fn: ProcessFn) -> None: + print(f"\nUsing filter: {filter_fn.__name__}") + image = Image.open(image_path) + image = filter_fn(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") diff --git a/2025/abstraction/abstraction_none/filters/grayscale.py b/2025/abstraction/abstraction_none/filters/grayscale.py new file mode 100644 index 00000000..50ac420f --- /dev/null +++ b/2025/abstraction/abstraction_none/filters/grayscale.py @@ -0,0 +1,22 @@ +from typing import Any + +from PIL import Image, ImageOps + + +class GrayscaleFilter: + def __init__(self) -> None: + self._intensity: float = 1.0 + + @property + def name(self) -> str: + return "Grayscale" + + def apply(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter with intensity {self._intensity}") + grayscale_image = ImageOps.grayscale(image) + if self._intensity < 1.0: + return Image.blend(image, grayscale_image, self._intensity) + return grayscale_image + + def configure(self, config: dict[str, Any]) -> None: + self._intensity = config.get("intensity", self._intensity) diff --git a/2025/abstraction/abstraction_none/filters/invert.py b/2025/abstraction/abstraction_none/filters/invert.py new file mode 100644 index 00000000..1f8f56c5 --- /dev/null +++ b/2025/abstraction/abstraction_none/filters/invert.py @@ -0,0 +1,11 @@ +from PIL import Image, ImageOps + + +class InvertFilter: + @property + def name(self) -> str: + return "Invert" + + def do_invert(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter") + return ImageOps.invert(image.convert("RGB")) diff --git a/2025/abstraction/abstraction_none/main.py b/2025/abstraction/abstraction_none/main.py index 2645b5a1..15e5589b 100644 --- a/2025/abstraction/abstraction_none/main.py +++ b/2025/abstraction/abstraction_none/main.py @@ -1,18 +1,18 @@ -from PIL import Image -from filters.grayscale import apply_grayscale -from filters.invert import apply_invert -from filters.sepia import apply_sepia +from filters.grayscale import GrayscaleFilter +from filters.invert import InvertFilter +from process_img import process_img def main() -> None: - image = Image.open("input.jpg") - image = apply_grayscale(image) - image = apply_invert(image) - image = apply_sepia(image) + input_image: str = "../input.jpg" - image.save("output.jpg") - print("Saved output.jpg") + grayscale = GrayscaleFilter() + grayscale.configure({"intensity": 0.8}) + process_img(input_image, "output_abc_grayscale.jpg", grayscale) + + invert = InvertFilter() + process_img(input_image, "output_abc_invert.jpg", invert) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/abstraction/abstraction_none/process_img.py b/2025/abstraction/abstraction_none/process_img.py new file mode 100644 index 00000000..66c8346d --- /dev/null +++ b/2025/abstraction/abstraction_none/process_img.py @@ -0,0 +1,20 @@ +from typing import Any + +from filters.grayscale import GrayscaleFilter +from filters.invert import InvertFilter +from PIL import Image + + +def process_img(image_path: str, output_path: str, filter_obj: Any) -> None: + print(f"\nUsing filter: {filter_obj.name}") + image = Image.open(image_path) + if isinstance(filter_obj, GrayscaleFilter): + filter_obj.configure({"intensity": 0.8}) + image = filter_obj.apply(image) + elif isinstance(filter_obj, InvertFilter): + image = filter_obj.do_invert(image) + else: + print("Unknown filter type. Skipping configuration.") + + image.save(output_path) + print(f"Saved processed image to {output_path}") diff --git a/2025/abstraction/abstraction_protocol.py b/2025/abstraction/abstraction_protocol.py deleted file mode 100644 index 59027783..00000000 --- a/2025/abstraction/abstraction_protocol.py +++ /dev/null @@ -1,63 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Protocol - -from PIL import Image - - -class ImageFilter(Protocol): - name: str - - def apply(self, image: Image.Image) -> Image.Image: ... - - def configure(self, config: dict[str, Any]) -> None: ... - - -@dataclass -class SepiaFilter: - name: str = "Sepia" - _depth: int = 20 - - def apply(self, image: Image.Image) -> Image.Image: - print(f"Applying {self.name} filter with depth {self._depth}") - sepia_image = image.convert("RGB") - width, height = sepia_image.size - pixels = sepia_image.load() - - for y in range(height): - for x in range(width): - r, g, b = pixels[x, y] - tr = int(0.393 * r + 0.769 * g + 0.189 * b + self._depth) - tg = int(0.349 * r + 0.686 * g + 0.168 * b + self._depth) - tb = int(0.272 * r + 0.534 * g + 0.131 * b + self._depth) - pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb)) - - return sepia_image - - def configure(self, config: dict[str, Any]) -> None: - self._depth = config.get("depth", self._depth) - - -def process_with_protocol( - image_path: str, output_path: str, filter_obj: ImageFilter -) -> None: - print(f"\nUsing filter: {filter_obj.name}") - image = Image.open(image_path) - image = filter_obj.apply(image) - image.save(output_path) - print(f"Saved processed image to {output_path}") - - -# ----------- Main function ----------- - - -def main() -> None: - input_image: str = "input.jpg" # Replace with a real image path - - # Protocol example - sepia = SepiaFilter() - sepia.configure({"depth": 15}) - process_with_protocol(input_image, "output_protocol_sepia.jpg", sepia) - - -if __name__ == "__main__": - main() diff --git a/2025/abstraction/abstraction_protocol/filters/grayscale.py b/2025/abstraction/abstraction_protocol/filters/grayscale.py new file mode 100644 index 00000000..50ac420f --- /dev/null +++ b/2025/abstraction/abstraction_protocol/filters/grayscale.py @@ -0,0 +1,22 @@ +from typing import Any + +from PIL import Image, ImageOps + + +class GrayscaleFilter: + def __init__(self) -> None: + self._intensity: float = 1.0 + + @property + def name(self) -> str: + return "Grayscale" + + def apply(self, image: Image.Image) -> Image.Image: + print(f"Applying {self.name} filter with intensity {self._intensity}") + grayscale_image = ImageOps.grayscale(image) + if self._intensity < 1.0: + return Image.blend(image, grayscale_image, self._intensity) + return grayscale_image + + def configure(self, config: dict[str, Any]) -> None: + self._intensity = config.get("intensity", self._intensity) diff --git a/2025/abstraction/abstraction_protocol/filters/invert.py b/2025/abstraction/abstraction_protocol/filters/invert.py new file mode 100644 index 00000000..856b0f6e --- /dev/null +++ b/2025/abstraction/abstraction_protocol/filters/invert.py @@ -0,0 +1,23 @@ +from typing import Any + +from PIL import Image, ImageOps + + +class InvertFilter: + def __init__(self) -> None: + self._enabled: bool = True + + @property + def name(self) -> str: + return "Invert" + + def apply(self, image: Image.Image) -> Image.Image: + if self._enabled: + print(f"Applying {self.name} filter") + return ImageOps.invert(image.convert("RGB")) + else: + print(f"{self.name} filter disabled, returning original image") + return image + + def configure(self, config: dict[str, Any]) -> None: + self._enabled = config.get("enabled", self._enabled) diff --git a/2025/abstraction/abstraction_protocol/main.py b/2025/abstraction/abstraction_protocol/main.py new file mode 100644 index 00000000..15e5589b --- /dev/null +++ b/2025/abstraction/abstraction_protocol/main.py @@ -0,0 +1,18 @@ +from filters.grayscale import GrayscaleFilter +from filters.invert import InvertFilter +from process_img import process_img + + +def main() -> None: + input_image: str = "../input.jpg" + + grayscale = GrayscaleFilter() + grayscale.configure({"intensity": 0.8}) + process_img(input_image, "output_abc_grayscale.jpg", grayscale) + + invert = InvertFilter() + process_img(input_image, "output_abc_invert.jpg", invert) + + +if __name__ == "__main__": + main() diff --git a/2025/abstraction/abstraction_protocol/process_img.py b/2025/abstraction/abstraction_protocol/process_img.py new file mode 100644 index 00000000..10c34f65 --- /dev/null +++ b/2025/abstraction/abstraction_protocol/process_img.py @@ -0,0 +1,20 @@ +from typing import Any, Protocol + +from PIL import Image + + +class FilterBase(Protocol): + @property + def name(self) -> str: ... + + def apply(self, image: Image.Image) -> Image.Image: ... + + def configure(self, config: dict[str, Any]) -> None: ... + + +def process_img(image_path: str, output_path: str, filter_obj: FilterBase) -> None: + print(f"\nUsing filter: {filter_obj.name}") + image = Image.open(image_path) + image = filter_obj.apply(image) + image.save(output_path) + print(f"Saved processed image to {output_path}") diff --git a/2025/abstraction/filters/grayscale.py b/2025/abstraction/filters/grayscale.py deleted file mode 100644 index bc3dcd63..00000000 --- a/2025/abstraction/filters/grayscale.py +++ /dev/null @@ -1,6 +0,0 @@ -from PIL import Image, ImageOps - - -def apply_grayscale(image: Image.Image) -> Image.Image: - print("Applying grayscale filter") - return ImageOps.grayscale(image) \ No newline at end of file From 2d72111489d958239bdbf5a2c93bbf8e6b29ae70 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 26 May 2025 14:54:42 +0200 Subject: [PATCH 014/113] Minor fixes. --- 2025/abstraction/abstraction_abc/filters/grayscale.py | 2 +- .../abstraction/abstraction_callable/filters/grayscale.py | 8 ++++---- 2025/abstraction/abstraction_callable/main.py | 8 ++++---- 2025/abstraction/abstraction_callable/process_img.py | 5 ++--- 2025/abstraction/abstraction_none/filters/grayscale.py | 2 +- .../abstraction/abstraction_protocol/filters/grayscale.py | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/2025/abstraction/abstraction_abc/filters/grayscale.py b/2025/abstraction/abstraction_abc/filters/grayscale.py index 6e7ecdd3..01dc662c 100644 --- a/2025/abstraction/abstraction_abc/filters/grayscale.py +++ b/2025/abstraction/abstraction_abc/filters/grayscale.py @@ -15,7 +15,7 @@ def name(self) -> str: def apply(self, image: Image.Image) -> Image.Image: print(f"Applying {self.name} filter with intensity {self._intensity}") - grayscale_image = ImageOps.grayscale(image) + grayscale_image = ImageOps.grayscale(image).convert("RGB") if self._intensity < 1.0: return Image.blend(image, grayscale_image, self._intensity) return grayscale_image diff --git a/2025/abstraction/abstraction_callable/filters/grayscale.py b/2025/abstraction/abstraction_callable/filters/grayscale.py index bdc0bf96..361c87e2 100644 --- a/2025/abstraction/abstraction_callable/filters/grayscale.py +++ b/2025/abstraction/abstraction_callable/filters/grayscale.py @@ -3,7 +3,7 @@ def apply_grayscale(image: Image.Image, intensity: float) -> Image.Image: print(f"Applying grayscale filter with intensity {intensity}") - grayscale_image = ImageOps.grayscale(image) - if intensity < 1.0: - return Image.blend(image, grayscale_image, intensity) - return grayscale_image + grayscale_image = ImageOps.grayscale(image).convert("RGB") + if intensity < 1.0: + return Image.blend(image, grayscale_image, intensity) + return grayscale_image diff --git a/2025/abstraction/abstraction_callable/main.py b/2025/abstraction/abstraction_callable/main.py index b0b92af7..d90dbee6 100644 --- a/2025/abstraction/abstraction_callable/main.py +++ b/2025/abstraction/abstraction_callable/main.py @@ -3,10 +3,10 @@ from filters.grayscale import apply_grayscale from filters.invert import apply_invert from PIL import Image, ImageOps -from process_img import process_img +from process_img import ImageFilterFn, process_img -def make_grayscale_filter(intensity: float = 1.0) -> ImageFilterFunc: +def make_grayscale_filter(intensity: float = 1.0) -> ImageFilterFn: def filter_func(image: Image.Image) -> Image.Image: print(f"Applying grayscale filter with intensity {intensity}") # Note: intensity isn't actually used by Pillow here, but it demonstrates config @@ -15,7 +15,7 @@ def filter_func(image: Image.Image) -> Image.Image: return filter_func -def make_invert_filter(enabled: bool = True) -> ImageFilterFunc: +def make_invert_filter(enabled: bool = True) -> ImageFilterFn: def filter_func(image: Image.Image) -> Image.Image: if enabled: print("Applying invert filter") @@ -30,7 +30,7 @@ def filter_func(image: Image.Image) -> Image.Image: def main() -> None: input_image: str = "../input.jpg" - grayscale_fn = partial(apply_grayscale, intensity=0.8) + grayscale_fn = partial(apply_grayscale, intensity=0.6) process_img(input_image, "output_abc_grayscale.jpg", grayscale_fn) process_img(input_image, "output_abc_invert.jpg", apply_invert) diff --git a/2025/abstraction/abstraction_callable/process_img.py b/2025/abstraction/abstraction_callable/process_img.py index 946fd09c..6ae7ec6c 100644 --- a/2025/abstraction/abstraction_callable/process_img.py +++ b/2025/abstraction/abstraction_callable/process_img.py @@ -2,11 +2,10 @@ from PIL import Image -type ProcessFn = Callable[[image.Image], image.Image] +type ImageFilterFn = Callable[[Image], Image] -def process_img(image_path: str, output_path: str, filter_fn: ProcessFn) -> None: - print(f"\nUsing filter: {filter_fn.__name__}") +def process_img(image_path: str, output_path: str, filter_fn: ImageFilterFn) -> None: image = Image.open(image_path) image = filter_fn(image) image.save(output_path) diff --git a/2025/abstraction/abstraction_none/filters/grayscale.py b/2025/abstraction/abstraction_none/filters/grayscale.py index 50ac420f..8946494b 100644 --- a/2025/abstraction/abstraction_none/filters/grayscale.py +++ b/2025/abstraction/abstraction_none/filters/grayscale.py @@ -13,7 +13,7 @@ def name(self) -> str: def apply(self, image: Image.Image) -> Image.Image: print(f"Applying {self.name} filter with intensity {self._intensity}") - grayscale_image = ImageOps.grayscale(image) + grayscale_image = ImageOps.grayscale(image).convert("RGB") if self._intensity < 1.0: return Image.blend(image, grayscale_image, self._intensity) return grayscale_image diff --git a/2025/abstraction/abstraction_protocol/filters/grayscale.py b/2025/abstraction/abstraction_protocol/filters/grayscale.py index 50ac420f..8946494b 100644 --- a/2025/abstraction/abstraction_protocol/filters/grayscale.py +++ b/2025/abstraction/abstraction_protocol/filters/grayscale.py @@ -13,7 +13,7 @@ def name(self) -> str: def apply(self, image: Image.Image) -> Image.Image: print(f"Applying {self.name} filter with intensity {self._intensity}") - grayscale_image = ImageOps.grayscale(image) + grayscale_image = ImageOps.grayscale(image).convert("RGB") if self._intensity < 1.0: return Image.blend(image, grayscale_image, self._intensity) return grayscale_image From beca4f693ef8ac097fd8e8e9926543cf3cfab565 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 27 May 2025 16:48:04 +0200 Subject: [PATCH 015/113] Created SDK design examples. --- 2025/sdk/pyproject.toml | 16 ++ 2025/sdk/sdk_v0_example.py | 13 ++ 2025/sdk/sdk_v1/__init__.py | 0 2025/sdk/sdk_v1/client.py | 25 +++ 2025/sdk/sdk_v1/user.py | 14 ++ 2025/sdk/sdk_v1_example.py | 16 ++ 2025/sdk/sdk_v2/__init__.py | 0 2025/sdk/sdk_v2/client.py | 37 +++++ 2025/sdk/sdk_v2/user.py | 14 ++ 2025/sdk/sdk_v2_example.py | 16 ++ 2025/sdk/sdk_v3/__init__.py | 0 2025/sdk/sdk_v3/base.py | 39 +++++ 2025/sdk/sdk_v3/client.py | 37 +++++ 2025/sdk/sdk_v3/user.py | 12 ++ 2025/sdk/sdk_v3_example.py | 16 ++ 2025/sdk/test_api.py | 89 +++++++++++ 2025/sdk/uv.lock | 309 ++++++++++++++++++++++++++++++++++++ 17 files changed, 653 insertions(+) create mode 100644 2025/sdk/pyproject.toml create mode 100644 2025/sdk/sdk_v0_example.py create mode 100644 2025/sdk/sdk_v1/__init__.py create mode 100644 2025/sdk/sdk_v1/client.py create mode 100644 2025/sdk/sdk_v1/user.py create mode 100644 2025/sdk/sdk_v1_example.py create mode 100644 2025/sdk/sdk_v2/__init__.py create mode 100644 2025/sdk/sdk_v2/client.py create mode 100644 2025/sdk/sdk_v2/user.py create mode 100644 2025/sdk/sdk_v2_example.py create mode 100644 2025/sdk/sdk_v3/__init__.py create mode 100644 2025/sdk/sdk_v3/base.py create mode 100644 2025/sdk/sdk_v3/client.py create mode 100644 2025/sdk/sdk_v3/user.py create mode 100644 2025/sdk/sdk_v3_example.py create mode 100644 2025/sdk/test_api.py create mode 100644 2025/sdk/uv.lock diff --git a/2025/sdk/pyproject.toml b/2025/sdk/pyproject.toml new file mode 100644 index 00000000..10719103 --- /dev/null +++ b/2025/sdk/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "sdk" +version = "0.1.0" +description = "SDK design" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115.12", + "httpx>=0.28.1", + "pydantic[email]>=2.11.5", + "uvicorn>=0.34.2", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", +] diff --git a/2025/sdk/sdk_v0_example.py b/2025/sdk/sdk_v0_example.py new file mode 100644 index 00000000..9f7e3261 --- /dev/null +++ b/2025/sdk/sdk_v0_example.py @@ -0,0 +1,13 @@ +import httpx + + +def main() -> None: + # Initialize the API client with your API + headers = {"Authorization": "Bearer secret123"} + response = httpx.get("http://localhost:8000/users", headers=headers) + users = response.json() + print(users) + + +if __name__ == "__main__": + main() diff --git a/2025/sdk/sdk_v1/__init__.py b/2025/sdk/sdk_v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/sdk/sdk_v1/client.py b/2025/sdk/sdk_v1/client.py new file mode 100644 index 00000000..0e65ea97 --- /dev/null +++ b/2025/sdk/sdk_v1/client.py @@ -0,0 +1,25 @@ +import httpx + + +class APIHttpClient: + def __init__(self, token: str, base_url: str = "http://localhost:8000"): + self.client = httpx.Client( + base_url=base_url, headers={"Authorization": f"Bearer {token}"} + ) + + def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response: + response = self.client.request(method, endpoint, **kwargs) + response.raise_for_status() + return response + + def get(self, endpoint: str, params: dict = None) -> httpx.Response: + return self.request("GET", endpoint, params=params) + + def post(self, endpoint: str, json: dict = None) -> httpx.Response: + return self.request("POST", endpoint, json=json) + + def put(self, endpoint: str, json: dict = None) -> httpx.Response: + return self.request("PUT", endpoint, json=json) + + def delete(self, endpoint: str) -> httpx.Response: + return self.request("DELETE", endpoint) diff --git a/2025/sdk/sdk_v1/user.py b/2025/sdk/sdk_v1/user.py new file mode 100644 index 00000000..d309d7ad --- /dev/null +++ b/2025/sdk/sdk_v1/user.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, EmailStr + +from .client import APIHttpClient + + +class User(BaseModel): + id: str + name: str + email: EmailStr + + @classmethod + def find(cls, client: APIHttpClient) -> list["User"]: + response = client.get("/users") + return [cls(**data) for data in response.json()] diff --git a/2025/sdk/sdk_v1_example.py b/2025/sdk/sdk_v1_example.py new file mode 100644 index 00000000..8a55dfea --- /dev/null +++ b/2025/sdk/sdk_v1_example.py @@ -0,0 +1,16 @@ +from sdk_v1.client import APIHttpClient +from sdk_v1.user import User + + +def main(): + # Initialize the API client with your API + client = APIHttpClient(token="secret123") + + # Fetch users from the API + users = User.find(client) + for user in users: + print(user) + + +if __name__ == "__main__": + main() diff --git a/2025/sdk/sdk_v2/__init__.py b/2025/sdk/sdk_v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/sdk/sdk_v2/client.py b/2025/sdk/sdk_v2/client.py new file mode 100644 index 00000000..35f7cb86 --- /dev/null +++ b/2025/sdk/sdk_v2/client.py @@ -0,0 +1,37 @@ +import httpx + +_client = None +_token = None +_base_url = "http://localhost:8000" + + +def set_credentials(token: str): + global _client, _token, _base_url + _token = token + _client = httpx.Client( + base_url=_base_url, headers={"Authorization": f"Bearer {_token}"} + ) + + +def request(method: str, endpoint: str, **kwargs) -> httpx.Response: + if _client is None: + raise RuntimeError("Credentials not set. Call set_credentials() first.") + response = _client.request(method, endpoint, **kwargs) + response.raise_for_status() + return response + + +def get(endpoint: str, params: dict = None) -> httpx.Response: + return request("GET", endpoint, params=params) + + +def post(endpoint: str, json: dict = None) -> httpx.Response: + return request("POST", endpoint, json=json) + + +def put(endpoint: str, json: dict = None) -> httpx.Response: + return request("PUT", endpoint, json=json) + + +def delete(endpoint: str) -> httpx.Response: + return request("DELETE", endpoint) diff --git a/2025/sdk/sdk_v2/user.py b/2025/sdk/sdk_v2/user.py new file mode 100644 index 00000000..d37cf07c --- /dev/null +++ b/2025/sdk/sdk_v2/user.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, EmailStr + +from sdk_v2 import client + + +class User(BaseModel): + id: str + name: str + email: EmailStr + + @classmethod + def find(cls) -> list["User"]: + response = client.get("/users") + return [cls(**data) for data in response.json()] diff --git a/2025/sdk/sdk_v2_example.py b/2025/sdk/sdk_v2_example.py new file mode 100644 index 00000000..27ee9f4a --- /dev/null +++ b/2025/sdk/sdk_v2_example.py @@ -0,0 +1,16 @@ +from sdk_v2.client import set_credentials +from sdk_v2.user import User + + +def main(): + # Initialize the API client with your API + set_credentials(token="secret123") + + # Fetch users from the API + users = User.find() + for user in users: + print(user) + + +if __name__ == "__main__": + main() diff --git a/2025/sdk/sdk_v3/__init__.py b/2025/sdk/sdk_v3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/sdk/sdk_v3/base.py b/2025/sdk/sdk_v3/base.py new file mode 100644 index 00000000..38313ff8 --- /dev/null +++ b/2025/sdk/sdk_v3/base.py @@ -0,0 +1,39 @@ +from typing import ClassVar, Type + +from pydantic import BaseModel + +from sdk_v3 import client + + +class BaseAPIModel[T](BaseModel): + id: str | None = None + _resource_path: ClassVar[str] = "" + + def save(self) -> None: + data = self.dict(exclude_unset=True) + if self.id: + response = client.put(f"/{self._resource_path}/{self.id}", json=data) + else: + response = client.post(f"/{self._resource_path}", json=data) + response.raise_for_status() + self.id = response.json()["id"] + + def delete(self) -> None: + if not self.id: + raise ValueError("Cannot delete unsaved resource.") + response = client.delete(f"/{self._resource_path}/{self.id}") + response.raise_for_status() + + @classmethod + def load(cls: Type[T], resource_id: str) -> T: + response = client.get(f"/{cls._resource_path}/{resource_id}") + if response.status_code == 404: + raise ValueError(f"{cls.__name__} not found.") + response.raise_for_status() + return cls(**response.json()) + + @classmethod + def find(cls: Type[T]) -> list[T]: + response = client.get(f"/{cls._resource_path}") + response.raise_for_status() + return [cls(**item) for item in response.json()] diff --git a/2025/sdk/sdk_v3/client.py b/2025/sdk/sdk_v3/client.py new file mode 100644 index 00000000..35f7cb86 --- /dev/null +++ b/2025/sdk/sdk_v3/client.py @@ -0,0 +1,37 @@ +import httpx + +_client = None +_token = None +_base_url = "http://localhost:8000" + + +def set_credentials(token: str): + global _client, _token, _base_url + _token = token + _client = httpx.Client( + base_url=_base_url, headers={"Authorization": f"Bearer {_token}"} + ) + + +def request(method: str, endpoint: str, **kwargs) -> httpx.Response: + if _client is None: + raise RuntimeError("Credentials not set. Call set_credentials() first.") + response = _client.request(method, endpoint, **kwargs) + response.raise_for_status() + return response + + +def get(endpoint: str, params: dict = None) -> httpx.Response: + return request("GET", endpoint, params=params) + + +def post(endpoint: str, json: dict = None) -> httpx.Response: + return request("POST", endpoint, json=json) + + +def put(endpoint: str, json: dict = None) -> httpx.Response: + return request("PUT", endpoint, json=json) + + +def delete(endpoint: str) -> httpx.Response: + return request("DELETE", endpoint) diff --git a/2025/sdk/sdk_v3/user.py b/2025/sdk/sdk_v3/user.py new file mode 100644 index 00000000..75923f28 --- /dev/null +++ b/2025/sdk/sdk_v3/user.py @@ -0,0 +1,12 @@ +from typing import ClassVar + +from pydantic import EmailStr + +from .base import BaseAPIModel + + +class User(BaseAPIModel["User"]): + name: str + email: EmailStr + + _resource_path: ClassVar[str] = "users" diff --git a/2025/sdk/sdk_v3_example.py b/2025/sdk/sdk_v3_example.py new file mode 100644 index 00000000..908f048f --- /dev/null +++ b/2025/sdk/sdk_v3_example.py @@ -0,0 +1,16 @@ +from sdk_v3.client import set_credentials +from sdk_v3.user import User + + +def main(): + # Initialize the API client with your API + set_credentials(token="secret123") + + # Fetch users from the API + users = User.find() + for user in users: + print(user) + + +if __name__ == "__main__": + main() diff --git a/2025/sdk/test_api.py b/2025/sdk/test_api.py new file mode 100644 index 00000000..e3b8fc55 --- /dev/null +++ b/2025/sdk/test_api.py @@ -0,0 +1,89 @@ +from typing import Optional +from uuid import uuid4 + +import uvicorn +from fastapi import Depends, FastAPI, Header, HTTPException, status +from pydantic import BaseModel, EmailStr + +app = FastAPI() + +# Simulated database +fake_users_db = {} + +# API key setup +API_KEY = "secret123" # You can change this to anything you like + + +def get_api_key(authorization: Optional[str] = Header(None)): + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing Authorization header", + ) + + token = authorization.split(" ")[1] + if token != API_KEY: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API key" + ) + + return token + + +# Pydantic models +class UserCreate(BaseModel): + name: str + email: EmailStr + + +class User(UserCreate): + id: str + + +# CRUD Endpoints +@app.post("/users", response_model=User) +def create_user(user: UserCreate, api_key: str = Depends(get_api_key)): + user_id = str(uuid4()) + new_user = User(id=user_id, **user.dict()) + fake_users_db[user_id] = new_user + return new_user + + +@app.get("/users", response_model=list[User]) +def get_users(api_key: str = Depends(get_api_key)): + return list(fake_users_db.values()) + + +@app.get("/users/{user_id}", response_model=User) +def get_user(user_id: str, api_key: str = Depends(get_api_key)): + user = fake_users_db.get(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +@app.put("/users/{user_id}", response_model=User) +def update_user( + user_id: str, user_update: UserCreate, api_key: str = Depends(get_api_key) +): + if user_id not in fake_users_db: + raise HTTPException(status_code=404, detail="User not found") + updated_user = User(id=user_id, **user_update.dict()) + fake_users_db[user_id] = updated_user + return updated_user + + +@app.delete("/users/{user_id}") +def delete_user(user_id: str, api_key: str = Depends(get_api_key)): + if user_id not in fake_users_db: + raise HTTPException(status_code=404, detail="User not found") + del fake_users_db[user_id] + return {"detail": "User deleted"} + + +def main(): + uvicorn.run("test_api:app", host="0.0.0.0", port=8000, reload=True) + + +if __name__ == "__main__": + main() diff --git a/2025/sdk/uv.lock b/2025/sdk/uv.lock new file mode 100644 index 00000000..3053da92 --- /dev/null +++ b/2025/sdk/uv.lock @@ -0,0 +1,309 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "sdk" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.11.5" }, + { name = "uvicorn", specifier = ">=0.34.2" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, +] From e45363b646a294254ab68cf7b00a749ac44fa14d Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 27 May 2025 17:31:10 +0200 Subject: [PATCH 016/113] Minor update to sdk example --- 2025/sdk/sdk_v3_example.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/2025/sdk/sdk_v3_example.py b/2025/sdk/sdk_v3_example.py index 908f048f..ae991969 100644 --- a/2025/sdk/sdk_v3_example.py +++ b/2025/sdk/sdk_v3_example.py @@ -6,10 +6,17 @@ def main(): # Initialize the API client with your API set_credentials(token="secret123") - # Fetch users from the API + # Create and save + u = User(name="Alice", email="alice@example.com") + u.save() + + # Change and save + u.name = "Alice Smith" + u.save() + + # Find all users users = User.find() - for user in users: - print(user) + print(users) if __name__ == "__main__": From 5cbec25a9dd912202c4e04cf374fe50edfebc6f9 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 14 May 2025 16:53:56 +0200 Subject: [PATCH 017/113] Worked on basic serverless example --- 2025/serverless/.gitignore | 2 + 2025/serverless/Pulumi.yaml | 6 + 2025/serverless/__main__.py | 58 +++ 2025/serverless/functions/channels.json | 20 ++ 2025/serverless/functions/main.py | 7 + 2025/serverless/functions/main_channels.py | 38 ++ 2025/serverless/pyproject.toml | 10 + 2025/serverless/requirements.txt | 6 + 2025/serverless/uv.lock | 398 +++++++++++++++++++++ 9 files changed, 545 insertions(+) create mode 100644 2025/serverless/.gitignore create mode 100644 2025/serverless/Pulumi.yaml create mode 100644 2025/serverless/__main__.py create mode 100644 2025/serverless/functions/channels.json create mode 100644 2025/serverless/functions/main.py create mode 100644 2025/serverless/functions/main_channels.py create mode 100644 2025/serverless/pyproject.toml create mode 100644 2025/serverless/requirements.txt create mode 100644 2025/serverless/uv.lock diff --git a/2025/serverless/.gitignore b/2025/serverless/.gitignore new file mode 100644 index 00000000..a3807e5b --- /dev/null +++ b/2025/serverless/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/2025/serverless/Pulumi.yaml b/2025/serverless/Pulumi.yaml new file mode 100644 index 00000000..d7da5789 --- /dev/null +++ b/2025/serverless/Pulumi.yaml @@ -0,0 +1,6 @@ +name: channels +runtime: + name: python + options: + virtualenv: venv +description: A minimal Google Cloud Python Pulumi program diff --git a/2025/serverless/__main__.py b/2025/serverless/__main__.py new file mode 100644 index 00000000..b049b972 --- /dev/null +++ b/2025/serverless/__main__.py @@ -0,0 +1,58 @@ +import os +import time + +import pulumi +from pulumi_gcp import cloudfunctions, storage + +# Disable rule for that module-level exports be ALL_CAPS, for legibility. +# pylint: disable=C0103 + +# File path to where the Cloud Function's source code is located. +PATH_TO_SOURCE_CODE = "./functions" + +# We will store the source code to the Cloud Function in a Google Cloud Storage bucket. +bucket = storage.Bucket("cf_demo_bucket", location="US", force_destroy=True) + +# The Cloud Function source code itself needs to be zipped up into an +# archive, which we create using the pulumi.AssetArchive primitive. +assets = {} +for file in os.listdir(PATH_TO_SOURCE_CODE): + location = os.path.join(PATH_TO_SOURCE_CODE, file) + asset = pulumi.FileAsset(path=location) + assets[file] = asset + +archive = pulumi.AssetArchive(assets=assets) + +# Create the single Cloud Storage object, which contains all of the function's +# source code. ("main.py" and "requirements.txt".) +source_archive_object = storage.BucketObject( + "eta_demo_object", + name=f"main.py-{time.time()}", + bucket=bucket.name, + source=archive, +) + +# Create the Cloud Function, deploying the source we just uploaded to Google +# Cloud Storage. +fxn = cloudfunctions.Function( + "eta_demo_function", + entry_point="hello_name", + region="us-central1", + runtime="python310", + source_archive_bucket=bucket.name, + source_archive_object=source_archive_object.name, + trigger_http=True, +) + +invoker = cloudfunctions.FunctionIamMember( + "invoker", + project=fxn.project, + region=fxn.region, + cloud_function=fxn.name, + role="roles/cloudfunctions.invoker", + member="allUsers", +) + +# Export the DNS name of the bucket and the cloud function URL. +pulumi.export("bucket_name", bucket.url) +pulumi.export("fxn_url", fxn.https_trigger_url) diff --git a/2025/serverless/functions/channels.json b/2025/serverless/functions/channels.json new file mode 100644 index 00000000..e536a19e --- /dev/null +++ b/2025/serverless/functions/channels.json @@ -0,0 +1,20 @@ +[ + { + "id": "codestackr", + "name": "codeSTACKr", + "tags": ["web development", "typescript"], + "description": "My tutorials are generally about web development and include coding languages such as HTML, CSS, Sass, JavaScript, and TypeScript." + }, + { + "id": "jackherrington", + "name": "Jack Herrington", + "tags": ["frontend", "technology"], + "description": "Frontend videos from basic to very advanced; tutorials, technology deep dives. You'll love it!" + }, + { + "id": "arjancodes", + "name": "ArjanCodes", + "tags": ["software design", "python"], + "description": "ArjanCodes focuses on helping you become a better software developer." + } +] \ No newline at end of file diff --git a/2025/serverless/functions/main.py b/2025/serverless/functions/main.py new file mode 100644 index 00000000..c722d1df --- /dev/null +++ b/2025/serverless/functions/main.py @@ -0,0 +1,7 @@ +import flask +import functions_framework + + +@functions_framework.http +def hello(request: flask.Request) -> flask.Response: + return flask.Response("Hello, World!", status=200) diff --git a/2025/serverless/functions/main_channels.py b/2025/serverless/functions/main_channels.py new file mode 100644 index 00000000..3332ca03 --- /dev/null +++ b/2025/serverless/functions/main_channels.py @@ -0,0 +1,38 @@ +import json +from typing import Any + +import flask +import functions_framework + +# Define an internal Flask app +app = flask.Flask("internal") + + +channels: dict[str, Any] = {} + +with open("channels.json", encoding="utf8") as file: + channels_raw = json.load(file) + for channel_raw in channels_raw: + channels[channel_raw["id"]] = channel_raw + + +# Define the internal path, idiomatic Flask definition +@app.route("/channels/", methods=["GET", "POST"]) +def name(channel_id: str): + if channel_id not in channels: + return flask.Response("Channel not found", status=404) + return flask.Response(json.dumps(channels[channel_id]), status=200) + + +@functions_framework.http +def hello(request: flask.Request) -> flask.Response: + # Create a new app context for the internal app + ctx = app.test_request_context( + path=request.full_path, + method=request.method, + ) + ctx.request = request + ctx.push() + response = app.full_dispatch_request() + ctx.pop() + return response diff --git a/2025/serverless/pyproject.toml b/2025/serverless/pyproject.toml new file mode 100644 index 00000000..eda0a670 --- /dev/null +++ b/2025/serverless/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "serverless" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "flask>=3.1.1", + "functions-framework>=3.8.2", + "pulumi>=3.169.0", + "pulumi-gcp>=8.30.1", +] diff --git a/2025/serverless/requirements.txt b/2025/serverless/requirements.txt new file mode 100644 index 00000000..480f1dce --- /dev/null +++ b/2025/serverless/requirements.txt @@ -0,0 +1,6 @@ +pulumi>=3.0.0,<4.0.0 +pulumi-gcp>=6.0.0,<7.0.0 +fastapi +pydantic +uvicorn +watchfiles \ No newline at end of file diff --git a/2025/serverless/uv.lock b/2025/serverless/uv.lock new file mode 100644 index 00000000..db27bd00 --- /dev/null +++ b/2025/serverless/uv.lock @@ -0,0 +1,398 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "arpeggio" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/c4/516bb54456f85ad1947702ea4cef543a59de66d31a9887dbc3d9df36e3e1/Arpeggio-2.0.2.tar.gz", hash = "sha256:c790b2b06e226d2dd468e4fbfb5b7f506cec66416031fde1441cf1de2a0ba700", size = 766643, upload-time = "2023-07-09T12:30:04.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/4f/d28bf30a19d4649b40b501d531b44e73afada99044df100380fd9567e92f/Arpeggio-2.0.2-py2.py3-none-any.whl", hash = "sha256:f7c8ae4f4056a89e020c24c7202ac8df3e2bc84e416746f20b0da35bb1de0250", size = 55287, upload-time = "2023-07-09T12:30:01.87Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, +] + +[[package]] +name = "cloudevents" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/41/97a7448adf5888d394a22d491749fb55b1e06e95870bd9edc3d58889bb8a/cloudevents-1.11.0.tar.gz", hash = "sha256:5be990583e99f3b08af5a709460e20b25cb169270227957a20b47a6ec8635e66", size = 33670, upload-time = "2024-06-20T13:47:32.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/0e/268a75b712e4dd504cff19e4b987942cd93532d1680009d6492c9d41bdac/cloudevents-1.11.0-py3-none-any.whl", hash = "sha256:77edb4f2b01f405c44ea77120c3213418dbc63d8859f98e9e85de875502b8a76", size = 55088, upload-time = "2024-06-20T13:47:30.066Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload-time = "2025-04-10T19:46:26.044Z" }, + { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload-time = "2025-04-10T19:46:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload-time = "2025-04-10T19:46:29.467Z" }, + { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload-time = "2025-04-10T19:46:31.538Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "flask" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, +] + +[[package]] +name = "functions-framework" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudevents" }, + { name = "flask" }, + { name = "gunicorn", marker = "sys_platform != 'win32'" }, + { name = "watchdog" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/80/b19ccfd7148a487c4583d03a3b9f21680cc19beb2059ec838209caa1d7b2/functions_framework-3.8.2.tar.gz", hash = "sha256:109bcdca01244067052a605536b44d042903b3805d093cd32e343ba5affffc90", size = 44392, upload-time = "2024-11-13T21:41:17.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/79/7e2391feb0fbfa2d1466944c030070fa4c7f5cac74e26680c42f5b622c21/functions_framework-3.8.2-py3-none-any.whl", hash = "sha256:ecbe8e4566efca9ed1718f210ac92d47fc47ec3a448d2bca3b4bb5888bceca08", size = 35963, upload-time = "2024-11-13T21:41:15.535Z" }, +] + +[[package]] +name = "grpcio" +version = "1.66.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/d1/49a96df4eb1d805cf546247df40636515416d2d5c66665e5129c8b4162a8/grpcio-1.66.2.tar.gz", hash = "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", size = 12489713, upload-time = "2024-09-28T12:44:01.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/5c/c4da36b7a77dbb15c4bc72228dff7161874752b2c6bddf7bb046d9da1b90/grpcio-1.66.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", size = 5002933, upload-time = "2024-09-28T12:38:24.109Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d5/b631445dff250a5301f51ff56c5fc917c7f955cd02fa55379f158a89abeb/grpcio-1.66.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", size = 10793953, upload-time = "2024-09-28T12:38:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/c8/1c/2179ac112152e92c02990f98183edf645df14aa3c38b39f1a3a60358b6c6/grpcio-1.66.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", size = 5499791, upload-time = "2024-09-28T12:38:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/0b/53/8d7ab865fbd983309c8242930f00b28a01047f70c2b2e4c79a5c92a46a08/grpcio-1.66.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", size = 6109606, upload-time = "2024-09-28T12:38:33.566Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/3dfb5a3ff540636d46b8b723345e923e8c553d9b3f6a8d1b09b0d915eb46/grpcio-1.66.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", size = 5762866, upload-time = "2024-09-28T12:38:36.023Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cb/c07493ad5dd73d51e4e15b0d483ff212dfec136ee1e4f3b49d115bdc7a13/grpcio-1.66.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", size = 6446819, upload-time = "2024-09-28T12:38:38.69Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5f/142e19db367a34ea0ee8a8451e43215d0a1a5dbffcfdcae8801f22903301/grpcio-1.66.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", size = 6040273, upload-time = "2024-09-28T12:38:41.348Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3b/12fcd752c55002e4b0e0a7bd5faec101bc0a4e3890be3f95a43353142481/grpcio-1.66.2-cp312-cp312-win32.whl", hash = "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", size = 3537988, upload-time = "2024-09-28T12:38:44.544Z" }, + { url = "https://files.pythonhosted.org/packages/f1/70/76bfea3faa862bfceccba255792e780691ff25b8227180759c9d38769379/grpcio-1.66.2-cp312-cp312-win_amd64.whl", hash = "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679", size = 4275553, upload-time = "2024-09-28T12:38:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/8708a8dfb3f1ac89926c27c5dd17412764157a2959dbc5a606eaf8ac71f6/grpcio-1.66.2-cp313-cp313-linux_armv7l.whl", hash = "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", size = 5004245, upload-time = "2024-09-28T12:38:50.596Z" }, + { url = "https://files.pythonhosted.org/packages/8b/37/0b57c3769efb3cc9ec97fcaa9f7243046660e7ed58c0faebc4ef315df92c/grpcio-1.66.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", size = 10756749, upload-time = "2024-09-28T12:38:54.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5a/425e995724a19a1b110340ed653bc7c5de8019d9fc84b3798a0f79c3eb31/grpcio-1.66.2-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", size = 5499666, upload-time = "2024-09-28T12:38:57.145Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e4/86a5c5ec40a6b683671a1d044ebca433812d99da8fcfc2889e9c43cecbd4/grpcio-1.66.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", size = 6109578, upload-time = "2024-09-28T12:38:59.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a86742f3deaa22385c3bff984c5947fc62d47d3fab26c508730037d027e5/grpcio-1.66.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", size = 5763274, upload-time = "2024-09-28T12:39:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/c3/61/b9a2a4345dea0a354c4ed8ac7aacbdd0ff986acbc8f92680213cf3d2faa3/grpcio-1.66.2-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", size = 6450416, upload-time = "2024-09-28T12:39:05.06Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/ad303ce75d8cd71d855a661519aa160ce42f27498f589f1ae6d9f8c5e8ac/grpcio-1.66.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", size = 6040045, upload-time = "2024-09-28T12:39:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b3/8db1873e3240ef1672ba87b89e949ece367089e29e4d221377bfdd288bd3/grpcio-1.66.2-cp313-cp313-win32.whl", hash = "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", size = 3537126, upload-time = "2024-09-28T12:39:10.655Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/133216989fe7e17caeafd7ff5b17cc82c4e722025d0b8d5d2290c11fe2e6/grpcio-1.66.2-cp313-cp313-win_amd64.whl", hash = "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", size = 4278018, upload-time = "2024-09-28T12:39:13.196Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "parver" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arpeggio" }, + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/e5/1c774688a90f0b76e872e30f6f1ba3f5e14056cd0d96a684047d4a986226/parver-0.5.tar.gz", hash = "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777", size = 26908, upload-time = "2023-10-03T21:06:54.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/4c/f98024021bef4d44dce3613feebd702c7ad8883f777ff8488384c59e9774/parver-0.5-py3-none-any.whl", hash = "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2", size = 15172, upload-time = "2023-10-03T21:06:52.796Z" }, +] + +[[package]] +name = "pip" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/63/84fdeac1f03864c2b8b9f0b7fe711c4af5f95759ee281d2026530086b2f5/protobuf-4.25.7.tar.gz", hash = "sha256:28f65ae8c14523cc2c76c1e91680958700d3eac69f45c96512c12c63d9a38807", size = 380612, upload-time = "2025-04-24T02:56:58.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ed/9a58076cfb8edc237c92617f1d3744660e9b4457d54f3c2fdf1a4bbae5c7/protobuf-4.25.7-cp310-abi3-win32.whl", hash = "sha256:dc582cf1a73a6b40aa8e7704389b8d8352da616bc8ed5c6cc614bdd0b5ce3f7a", size = 392457, upload-time = "2025-04-24T02:56:40.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/b3/e00870528029fe252cf3bd6fa535821c276db3753b44a4691aee0d52ff9e/protobuf-4.25.7-cp310-abi3-win_amd64.whl", hash = "sha256:cd873dbddb28460d1706ff4da2e7fac175f62f2a0bebc7b33141f7523c5a2399", size = 413446, upload-time = "2025-04-24T02:56:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/60/1d/f450a193f875a20099d4492d2c1cb23091d65d512956fb1e167ee61b4bf0/protobuf-4.25.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4c899f09b0502eb39174c717ccf005b844ea93e31137c167ddcacf3e09e49610", size = 394248, upload-time = "2025-04-24T02:56:45.75Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/ea88e9857484a0618c74121618b9e620fc50042de43cdabbebe1b93a83e0/protobuf-4.25.7-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:6d2f5dede3d112e573f0e5f9778c0c19d9f9e209727abecae1d39db789f522c6", size = 293717, upload-time = "2025-04-24T02:56:47.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/81/d0b68e9a9a76804113b6dedc6fffed868b97048bbe6f1bedc675bdb8523c/protobuf-4.25.7-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:d41fb7ae72a25fcb79b2d71e4247f0547a02e8185ed51587c22827a87e5736ed", size = 294636, upload-time = "2025-04-24T02:56:48.976Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/1e7c80cb2ea2880cfe38580dcfbb22b78b746640c9c13fc3337a6967dc4c/protobuf-4.25.7-py3-none-any.whl", hash = "sha256:e9d969f5154eaeab41404def5dcf04e62162178f4b9de98b2d3c1c70f5f84810", size = 156468, upload-time = "2025-04-24T02:56:56.957Z" }, +] + +[[package]] +name = "pulumi" +version = "3.169.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "debugpy" }, + { name = "dill" }, + { name = "grpcio" }, + { name = "pip" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "semver" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f7/fdf271f5c576f380745c20c47812e98956f0871d3569125c93476ab44418/pulumi-3.169.0-py3-none-any.whl", hash = "sha256:8288d5baf63d280793d118c2df5708ed24327e59f20ecf3c86fc0900da2e8c38", size = 337496, upload-time = "2025-05-08T11:53:42.854Z" }, +] + +[[package]] +name = "pulumi-gcp" +version = "8.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/37/5e45019b81317c98ebba738104777edc77a698708292213315af6bf8098d/pulumi_gcp-8.30.1.tar.gz", hash = "sha256:e32f21d47d35439140a47a467cbabee66790d57d8c7f900e51ee612129168035", size = 7586280, upload-time = "2025-05-13T12:51:37.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/fd/395e816595575576bf4dd29c44d03ce5402aebdb56ac6f0813f55b76e1df/pulumi_gcp-8.30.1-py3-none-any.whl", hash = "sha256:71687a8f16c0e05846d699956c7da79ed5b633afab3a65671de0163167a6c948", size = 9603797, upload-time = "2025-05-13T12:51:33.698Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "serverless" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "functions-framework" }, + { name = "pulumi" }, + { name = "pulumi-gcp" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.1.1" }, + { name = "functions-framework", specifier = ">=3.8.2" }, + { name = "pulumi", specifier = ">=3.169.0" }, + { name = "pulumi-gcp", specifier = ">=8.30.1" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] From 1b78eef07cd2a71e21259d74e99b81b9242f988e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 20 May 2025 15:40:56 +0200 Subject: [PATCH 018/113] Changed example to use 2nd gen cloud run functions. Cleanup. --- 2025/serverless/.gitignore | 2 - 2025/serverless/Pulumi.testing.yaml | 3 + 2025/serverless/Pulumi.yaml | 7 +- 2025/serverless/__main__.py | 80 ++++++++----------- .../{functions => function}/channels.json | 0 .../{functions/main.py => function/hello.py} | 2 +- .../main_channels.py => function/main.py} | 2 +- 2025/serverless/pyproject.toml | 9 +-- 2025/serverless/requirements.txt | 6 -- 2025/serverless/uv.lock | 61 +++----------- 10 files changed, 58 insertions(+), 114 deletions(-) delete mode 100644 2025/serverless/.gitignore create mode 100644 2025/serverless/Pulumi.testing.yaml rename 2025/serverless/{functions => function}/channels.json (100%) rename 2025/serverless/{functions/main.py => function/hello.py} (66%) rename 2025/serverless/{functions/main_channels.py => function/main.py} (93%) delete mode 100644 2025/serverless/requirements.txt diff --git a/2025/serverless/.gitignore b/2025/serverless/.gitignore deleted file mode 100644 index a3807e5b..00000000 --- a/2025/serverless/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -venv/ diff --git a/2025/serverless/Pulumi.testing.yaml b/2025/serverless/Pulumi.testing.yaml new file mode 100644 index 00000000..346cf126 --- /dev/null +++ b/2025/serverless/Pulumi.testing.yaml @@ -0,0 +1,3 @@ +config: + gcp:project: acpulumi-460409 + gcp:region: europe-west1 diff --git a/2025/serverless/Pulumi.yaml b/2025/serverless/Pulumi.yaml index d7da5789..c84cacb8 100644 --- a/2025/serverless/Pulumi.yaml +++ b/2025/serverless/Pulumi.yaml @@ -1,6 +1,7 @@ -name: channels +name: serverless-py runtime: name: python options: - virtualenv: venv -description: A minimal Google Cloud Python Pulumi program + toolchain: uv + virtualenv: .venv +description: Basic example of a serverless Python and Go GCP functions (in Python) \ No newline at end of file diff --git a/2025/serverless/__main__.py b/2025/serverless/__main__.py index b049b972..d72b9f99 100644 --- a/2025/serverless/__main__.py +++ b/2025/serverless/__main__.py @@ -1,58 +1,44 @@ -import os -import time +from pulumi import asset, export +from pulumi_gcp import cloudfunctionsv2, storage -import pulumi -from pulumi_gcp import cloudfunctions, storage +bucket = storage.Bucket("bucket", location="europe-west1") -# Disable rule for that module-level exports be ALL_CAPS, for legibility. -# pylint: disable=C0103 - -# File path to where the Cloud Function's source code is located. -PATH_TO_SOURCE_CODE = "./functions" - -# We will store the source code to the Cloud Function in a Google Cloud Storage bucket. -bucket = storage.Bucket("cf_demo_bucket", location="US", force_destroy=True) - -# The Cloud Function source code itself needs to be zipped up into an -# archive, which we create using the pulumi.AssetArchive primitive. -assets = {} -for file in os.listdir(PATH_TO_SOURCE_CODE): - location = os.path.join(PATH_TO_SOURCE_CODE, file) - asset = pulumi.FileAsset(path=location) - assets[file] = asset - -archive = pulumi.AssetArchive(assets=assets) - -# Create the single Cloud Storage object, which contains all of the function's -# source code. ("main.py" and "requirements.txt".) -source_archive_object = storage.BucketObject( - "eta_demo_object", - name=f"main.py-{time.time()}", +py_bucket_object = storage.BucketObject( + "python-zip", bucket=bucket.name, - source=archive, + source=asset.AssetArchive({".": asset.FileArchive("./function")}), ) -# Create the Cloud Function, deploying the source we just uploaded to Google -# Cloud Storage. -fxn = cloudfunctions.Function( - "eta_demo_function", - entry_point="hello_name", - region="us-central1", - runtime="python310", - source_archive_bucket=bucket.name, - source_archive_object=source_archive_object.name, - trigger_http=True, +py_function = cloudfunctionsv2.Function( + "python-func", + name="python-func", + location="europe-west1", + build_config={ + "runtime": "python313", + "entry_point": "channels_handler", + "source": { + "storage_source": { + "bucket": bucket.name, + "object": py_bucket_object.name, + }, + }, + }, + service_config={ + "max_instance_count": 1, + "available_memory": "256M", + "timeout_seconds": 60, + }, ) -invoker = cloudfunctions.FunctionIamMember( - "invoker", - project=fxn.project, - region=fxn.region, - cloud_function=fxn.name, + +py_invoker = cloudfunctionsv2.FunctionIamMember( + "py-invoker", + project=py_function.project, + location=py_function.location, + cloud_function=py_function.name, role="roles/cloudfunctions.invoker", + # role="roles/run.invoker", member="allUsers", ) -# Export the DNS name of the bucket and the cloud function URL. -pulumi.export("bucket_name", bucket.url) -pulumi.export("fxn_url", fxn.https_trigger_url) +export("python_endpoint", py_function.url) diff --git a/2025/serverless/functions/channels.json b/2025/serverless/function/channels.json similarity index 100% rename from 2025/serverless/functions/channels.json rename to 2025/serverless/function/channels.json diff --git a/2025/serverless/functions/main.py b/2025/serverless/function/hello.py similarity index 66% rename from 2025/serverless/functions/main.py rename to 2025/serverless/function/hello.py index c722d1df..cba3e5da 100644 --- a/2025/serverless/functions/main.py +++ b/2025/serverless/function/hello.py @@ -3,5 +3,5 @@ @functions_framework.http -def hello(request: flask.Request) -> flask.Response: +def hello_handler(request: flask.Request) -> flask.Response: return flask.Response("Hello, World!", status=200) diff --git a/2025/serverless/functions/main_channels.py b/2025/serverless/function/main.py similarity index 93% rename from 2025/serverless/functions/main_channels.py rename to 2025/serverless/function/main.py index 3332ca03..b1d882b5 100644 --- a/2025/serverless/functions/main_channels.py +++ b/2025/serverless/function/main.py @@ -25,7 +25,7 @@ def name(channel_id: str): @functions_framework.http -def hello(request: flask.Request) -> flask.Response: +def channels_handler(request: flask.Request) -> flask.Response: # Create a new app context for the internal app ctx = app.test_request_context( path=request.full_path, diff --git a/2025/serverless/pyproject.toml b/2025/serverless/pyproject.toml index eda0a670..5920e6f8 100644 --- a/2025/serverless/pyproject.toml +++ b/2025/serverless/pyproject.toml @@ -1,10 +1,9 @@ [project] name = "serverless" version = "0.1.0" -requires-python = ">=3.12" +requires-python = ">=3.13" dependencies = [ - "flask>=3.1.1", - "functions-framework>=3.8.2", - "pulumi>=3.169.0", - "pulumi-gcp>=8.30.1", + "functions-framework>=3.8.3", + "pulumi>=3.170.0", + "pulumi-gcp>=8.31.0", ] diff --git a/2025/serverless/requirements.txt b/2025/serverless/requirements.txt deleted file mode 100644 index 480f1dce..00000000 --- a/2025/serverless/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pulumi>=3.0.0,<4.0.0 -pulumi-gcp>=6.0.0,<7.0.0 -fastapi -pydantic -uvicorn -watchfiles \ No newline at end of file diff --git a/2025/serverless/uv.lock b/2025/serverless/uv.lock index db27bd00..fad3ea17 100644 --- a/2025/serverless/uv.lock +++ b/2025/serverless/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.12" +requires-python = ">=3.13" [[package]] name = "arpeggio" @@ -68,10 +68,6 @@ version = "1.8.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload-time = "2025-04-10T19:46:26.044Z" }, - { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload-time = "2025-04-10T19:46:27.464Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload-time = "2025-04-10T19:46:29.467Z" }, - { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload-time = "2025-04-10T19:46:31.538Z" }, { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, @@ -119,7 +115,7 @@ wheels = [ [[package]] name = "functions-framework" -version = "3.8.2" +version = "3.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -129,9 +125,9 @@ dependencies = [ { name = "watchdog" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/80/b19ccfd7148a487c4583d03a3b9f21680cc19beb2059ec838209caa1d7b2/functions_framework-3.8.2.tar.gz", hash = "sha256:109bcdca01244067052a605536b44d042903b3805d093cd32e343ba5affffc90", size = 44392, upload-time = "2024-11-13T21:41:17.467Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/1f/367a737a58b53f22113e7f443598a3907d67a5ddf1c45f9d35a6ba642bb0/functions_framework-3.8.3.tar.gz", hash = "sha256:95827698469e3979518d52e32def2f11230465877fef32afd49045013b2a469c", size = 43640, upload-time = "2025-05-16T18:27:34.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/79/7e2391feb0fbfa2d1466944c030070fa4c7f5cac74e26680c42f5b622c21/functions_framework-3.8.2-py3-none-any.whl", hash = "sha256:ecbe8e4566efca9ed1718f210ac92d47fc47ec3a448d2bca3b4bb5888bceca08", size = 35963, upload-time = "2024-11-13T21:41:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/26/52/b0b1fe1b964a9afe6b7da41c88434f09c16bf915cd48b9185e839f96fa6c/functions_framework-3.8.3-py3-none-any.whl", hash = "sha256:fd352272c02ee08b4a3445e234213e43fbdd350365d98e2cce13e1575490bffe", size = 36051, upload-time = "2025-05-16T18:27:33.037Z" }, ] [[package]] @@ -140,15 +136,6 @@ version = "1.66.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/71/d1/49a96df4eb1d805cf546247df40636515416d2d5c66665e5129c8b4162a8/grpcio-1.66.2.tar.gz", hash = "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", size = 12489713, upload-time = "2024-09-28T12:44:01.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/5c/c4da36b7a77dbb15c4bc72228dff7161874752b2c6bddf7bb046d9da1b90/grpcio-1.66.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", size = 5002933, upload-time = "2024-09-28T12:38:24.109Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d5/b631445dff250a5301f51ff56c5fc917c7f955cd02fa55379f158a89abeb/grpcio-1.66.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", size = 10793953, upload-time = "2024-09-28T12:38:27.02Z" }, - { url = "https://files.pythonhosted.org/packages/c8/1c/2179ac112152e92c02990f98183edf645df14aa3c38b39f1a3a60358b6c6/grpcio-1.66.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", size = 5499791, upload-time = "2024-09-28T12:38:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/0b/53/8d7ab865fbd983309c8242930f00b28a01047f70c2b2e4c79a5c92a46a08/grpcio-1.66.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", size = 6109606, upload-time = "2024-09-28T12:38:33.566Z" }, - { url = "https://files.pythonhosted.org/packages/86/e9/3dfb5a3ff540636d46b8b723345e923e8c553d9b3f6a8d1b09b0d915eb46/grpcio-1.66.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", size = 5762866, upload-time = "2024-09-28T12:38:36.023Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cb/c07493ad5dd73d51e4e15b0d483ff212dfec136ee1e4f3b49d115bdc7a13/grpcio-1.66.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", size = 6446819, upload-time = "2024-09-28T12:38:38.69Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5f/142e19db367a34ea0ee8a8451e43215d0a1a5dbffcfdcae8801f22903301/grpcio-1.66.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", size = 6040273, upload-time = "2024-09-28T12:38:41.348Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3b/12fcd752c55002e4b0e0a7bd5faec101bc0a4e3890be3f95a43353142481/grpcio-1.66.2-cp312-cp312-win32.whl", hash = "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", size = 3537988, upload-time = "2024-09-28T12:38:44.544Z" }, - { url = "https://files.pythonhosted.org/packages/f1/70/76bfea3faa862bfceccba255792e780691ff25b8227180759c9d38769379/grpcio-1.66.2-cp312-cp312-win_amd64.whl", hash = "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679", size = 4275553, upload-time = "2024-09-28T12:38:47.734Z" }, { url = "https://files.pythonhosted.org/packages/72/31/8708a8dfb3f1ac89926c27c5dd17412764157a2959dbc5a606eaf8ac71f6/grpcio-1.66.2-cp313-cp313-linux_armv7l.whl", hash = "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", size = 5004245, upload-time = "2024-09-28T12:38:50.596Z" }, { url = "https://files.pythonhosted.org/packages/8b/37/0b57c3769efb3cc9ec97fcaa9f7243046660e7ed58c0faebc4ef315df92c/grpcio-1.66.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", size = 10756749, upload-time = "2024-09-28T12:38:54.131Z" }, { url = "https://files.pythonhosted.org/packages/bf/5a/425e995724a19a1b110340ed653bc7c5de8019d9fc84b3798a0f79c3eb31/grpcio-1.66.2-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", size = 5499666, upload-time = "2024-09-28T12:38:57.145Z" }, @@ -199,16 +186,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, @@ -278,7 +255,7 @@ wheels = [ [[package]] name = "pulumi" -version = "3.169.0" +version = "3.170.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debugpy" }, @@ -290,21 +267,21 @@ dependencies = [ { name = "semver" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f7/fdf271f5c576f380745c20c47812e98956f0871d3569125c93476ab44418/pulumi-3.169.0-py3-none-any.whl", hash = "sha256:8288d5baf63d280793d118c2df5708ed24327e59f20ecf3c86fc0900da2e8c38", size = 337496, upload-time = "2025-05-08T11:53:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/44/f0/b23f19dae871bb3d43bf3f29b11535d536d2ef9a4b7703e4d2467afc6cf9/pulumi-3.170.0-py3-none-any.whl", hash = "sha256:824b3c293c659be0cd6b975a719d921987a359f4c97639fc7ecd24a83e8cfd4d", size = 338393, upload-time = "2025-05-15T15:00:49.237Z" }, ] [[package]] name = "pulumi-gcp" -version = "8.30.1" +version = "8.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parver" }, { name = "pulumi" }, { name = "semver" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/37/5e45019b81317c98ebba738104777edc77a698708292213315af6bf8098d/pulumi_gcp-8.30.1.tar.gz", hash = "sha256:e32f21d47d35439140a47a467cbabee66790d57d8c7f900e51ee612129168035", size = 7586280, upload-time = "2025-05-13T12:51:37.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/ed/70faa2aeb61ae4cd460100303253577df08aad376fc66684248d8bd08643/pulumi_gcp-8.31.0.tar.gz", hash = "sha256:c32791fb029591217d95460b33368648b12879058830a0611b9b4f102a40ff8d", size = 7639978, upload-time = "2025-05-15T14:48:23.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/fd/395e816595575576bf4dd29c44d03ce5402aebdb56ac6f0813f55b76e1df/pulumi_gcp-8.30.1-py3-none-any.whl", hash = "sha256:71687a8f16c0e05846d699956c7da79ed5b633afab3a65671de0163167a6c948", size = 9603797, upload-time = "2025-05-13T12:51:33.698Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/1d122e3b6fc6a533e07480ae7ab24bd3bfb9489b9d121bd03bbfc95bb1ea/pulumi_gcp-8.31.0-py3-none-any.whl", hash = "sha256:10e72436757491d7e00ca3ea58541cf67b5c502ba6ec30481b0244481385ab43", size = 9658676, upload-time = "2025-05-15T14:48:20.098Z" }, ] [[package]] @@ -313,15 +290,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, @@ -347,7 +315,6 @@ name = "serverless" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "flask" }, { name = "functions-framework" }, { name = "pulumi" }, { name = "pulumi-gcp" }, @@ -355,10 +322,9 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "flask", specifier = ">=3.1.1" }, - { name = "functions-framework", specifier = ">=3.8.2" }, - { name = "pulumi", specifier = ">=3.169.0" }, - { name = "pulumi-gcp", specifier = ">=8.30.1" }, + { name = "functions-framework", specifier = ">=3.8.3" }, + { name = "pulumi", specifier = ">=3.170.0" }, + { name = "pulumi-gcp", specifier = ">=8.31.0" }, ] [[package]] @@ -367,9 +333,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, From 7355d456ef1786bd65cca7515722af2d23f2db2a Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 May 2025 15:07:57 +0200 Subject: [PATCH 019/113] Fixed serverless Pulumi example. --- 2025/serverless/__main__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/2025/serverless/__main__.py b/2025/serverless/__main__.py index d72b9f99..8e821a3d 100644 --- a/2025/serverless/__main__.py +++ b/2025/serverless/__main__.py @@ -1,5 +1,5 @@ from pulumi import asset, export -from pulumi_gcp import cloudfunctionsv2, storage +from pulumi_gcp import cloudfunctionsv2, cloudrunv2, storage bucket = storage.Bucket("bucket", location="europe-west1") @@ -10,8 +10,8 @@ ) py_function = cloudfunctionsv2.Function( - "python-func", - name="python-func", + "channels-api", + name="channels-api", location="europe-west1", build_config={ "runtime": "python313", @@ -30,14 +30,13 @@ }, ) - -py_invoker = cloudfunctionsv2.FunctionIamMember( +py_invoker = cloudrunv2.ServiceIamMember( "py-invoker", project=py_function.project, location=py_function.location, - cloud_function=py_function.name, - role="roles/cloudfunctions.invoker", - # role="roles/run.invoker", + name=py_function.name, + # role="roles/cloudfunctions.invoker", + role="roles/run.invoker", member="allUsers", ) From 1c55cf1be1009487fadba4214a26f3cdcd21c4e6 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 15 May 2025 16:23:22 +0200 Subject: [PATCH 020/113] Added mcp example --- 2025/mcp-server/pyproject.toml | 9 + 2025/mcp-server/uv.lock | 421 ++++++++++++++++++++++++++++ 2025/mcp-server/video_repository.py | 9 + 2025/mcp-server/videos.py | 39 +++ 4 files changed, 478 insertions(+) create mode 100644 2025/mcp-server/pyproject.toml create mode 100644 2025/mcp-server/uv.lock create mode 100644 2025/mcp-server/video_repository.py create mode 100644 2025/mcp-server/videos.py diff --git a/2025/mcp-server/pyproject.toml b/2025/mcp-server/pyproject.toml new file mode 100644 index 00000000..ca38033b --- /dev/null +++ b/2025/mcp-server/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "mcp-example" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.8.1", + "youtube-search>=2.1.2", +] diff --git a/2025/mcp-server/uv.lock b/2025/mcp-server/uv.lock new file mode 100644 index 00000000..62b22433 --- /dev/null +++ b/2025/mcp-server/uv.lock @@ -0,0 +1,421 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mcp" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/13/16b712e8a3be6a736b411df2fc6b4e75eb1d3e99b1cd57a3a1decf17f612/mcp-1.8.1.tar.gz", hash = "sha256:ec0646271d93749f784d2316fb5fe6102fb0d1be788ec70a9e2517e8f2722c0e", size = 265605, upload-time = "2025-05-12T17:33:57.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/5d/91cf0d40e40ae9ecf8d4004e0f9611eea86085aa0b5505493e0ff53972da/mcp-1.8.1-py3-none-any.whl", hash = "sha256:948e03783859fa35abe05b9b6c0a1d5519be452fc079dc8d7f682549591c1770", size = 119761, upload-time = "2025-05-12T17:33:56.136Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mcp-example" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "mcp", extra = ["cli"] }, + { name = "youtube-search" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.8.1" }, + { name = "youtube-search", specifier = ">=2.1.2" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "typer" +version = "0.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload-time = "2025-04-28T21:40:59.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, +] + +[[package]] +name = "youtube-search" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/66/87d6d426e6e7cbd9b675d712f2538d55023cb9394411e54cdb60cdc9e002/youtube-search-2.1.2.tar.gz", hash = "sha256:5749a6d8076fda65557c91367f0fab0936290ecdea457c831c9f3f05918a3aa2", size = 3167, upload-time = "2022-10-05T01:47:44.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4e/548b906dd72bb51f700feb4f2200de3e0c64b34509d3173afb196ad7f1b9/youtube_search-2.1.2-py3-none-any.whl", hash = "sha256:f9cfaaf7c59806777eb29d3b8acee96b50e80f73a0621c2bb220d4e5beb09e4f", size = 3395, upload-time = "2022-10-05T01:47:42.63Z" }, +] diff --git a/2025/mcp-server/video_repository.py b/2025/mcp-server/video_repository.py new file mode 100644 index 00000000..21b225b9 --- /dev/null +++ b/2025/mcp-server/video_repository.py @@ -0,0 +1,9 @@ +from typing import Any + +from youtube_search import YoutubeSearch + + +def search_youtube(query: str, max_results: int = 10) -> dict[str, Any]: + """Search YouTube for a given query and return the results.""" + results = YoutubeSearch(query, max_results=max_results).to_dict() + return results diff --git a/2025/mcp-server/videos.py b/2025/mcp-server/videos.py new file mode 100644 index 00000000..f244e552 --- /dev/null +++ b/2025/mcp-server/videos.py @@ -0,0 +1,39 @@ +from mcp.server.fastmcp import FastMCP +from video_repository import search_youtube + +# Initialize FastMCP server +mcp = FastMCP("videos") + + +def format_video(video: dict[str, str]) -> str: + """Format a video feature into a readable string.""" + return f""" + Title: {video.get("title", "Unknown")} + Channel: {video.get("channel", "Unknown")} + Duration: {video.get("duration", "Unknown")} + Description: {video.get("description", "No description available")} + Views: {video.get("views", "Unknown")} + URL: https://www.youtube.com/watch?v={video.get("id", "Unknown")} + Published: {video.get("publish_time", "Unknown")} + """ + + +@mcp.tool() +async def get_videos(search: str, max_results: int) -> str: + """Get videos for a search query. + + Args: + search: Search query string + max_results: Maximum number of results to return + """ + results = search_youtube(search, max_results=max_results) + if not results: + return "No videos found." + + videos = [format_video(video) for video in results] + return "\n---\n".join(videos) + + +if __name__ == "__main__": + # Initialize and run the server + mcp.run(transport="stdio") From 61973a5985e329e49fc611b9abe5a96eecbd34ac Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 16 May 2025 16:34:48 +0200 Subject: [PATCH 021/113] Added API example + MCP server that integrates with API --- 2025/mcp-server/README.md | 3 + 2025/mcp-server/{videos.py => mcp_videos.py} | 2 +- 2025/mcp-server/mcp_videos_api.py | 49 ++++ 2025/mcp-server/pyproject.toml | 2 + 2025/mcp-server/uv.lock | 223 ++++++++++++++++++ 2025/mcp-server/video_api.py | 52 ++++ .../{video_repository.py => yt_helper.py} | 2 +- 7 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 2025/mcp-server/README.md rename 2025/mcp-server/{videos.py => mcp_videos.py} (96%) create mode 100644 2025/mcp-server/mcp_videos_api.py create mode 100644 2025/mcp-server/video_api.py rename 2025/mcp-server/{video_repository.py => yt_helper.py} (73%) diff --git a/2025/mcp-server/README.md b/2025/mcp-server/README.md new file mode 100644 index 00000000..fd345324 --- /dev/null +++ b/2025/mcp-server/README.md @@ -0,0 +1,3 @@ +# Sample CURL request to API + +curl -X GET "http://localhost:8000/videos?search=python%20design%20patterns&max_results=3" -H "accept: application/json" \ No newline at end of file diff --git a/2025/mcp-server/videos.py b/2025/mcp-server/mcp_videos.py similarity index 96% rename from 2025/mcp-server/videos.py rename to 2025/mcp-server/mcp_videos.py index f244e552..3a3c1768 100644 --- a/2025/mcp-server/videos.py +++ b/2025/mcp-server/mcp_videos.py @@ -1,5 +1,5 @@ from mcp.server.fastmcp import FastMCP -from video_repository import search_youtube +from yt_helper import search_youtube # Initialize FastMCP server mcp = FastMCP("videos") diff --git a/2025/mcp-server/mcp_videos_api.py b/2025/mcp-server/mcp_videos_api.py new file mode 100644 index 00000000..9fa35425 --- /dev/null +++ b/2025/mcp-server/mcp_videos_api.py @@ -0,0 +1,49 @@ +import httpx +from mcp.server.fastmcp import FastMCP + +# Initialize FastMCP server +mcp = FastMCP("videos") + +API_URL = "http://localhost:8000/videos" # Adjust if hosted elsewhere + + +def format_video(video: dict[str, str]) -> str: + """Format a video feature into a readable string.""" + return f""" +Title: {video.get("title", "Unknown")} +Channel: {video.get("channel", "Unknown")} +Duration: {video.get("duration", "Unknown")} +Description: {video.get("description", "No description available")} +Views: {video.get("views", "Unknown")} +URL: {video.get("url", "Unknown")} +Published: {video.get("publish_time", "Unknown")} +""".strip() + + +@mcp.tool() +async def get_videos(search: str, max_results: int) -> str: + """Get videos for a search query via external FastAPI endpoint. + + Args: + search: Search query string + max_results: Maximum number of results to return + """ + async with httpx.AsyncClient() as client: + try: + response = await client.get( + API_URL, params={"search": search, "max_results": max_results} + ) + response.raise_for_status() + data = response.json() + videos = data.get("results", []) + except Exception as e: + return f"Error retrieving videos: {str(e)}" + + if not videos: + return "No videos found." + + return "\n---\n".join(format_video(video) for video in videos) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/2025/mcp-server/pyproject.toml b/2025/mcp-server/pyproject.toml index ca38033b..7a88121d 100644 --- a/2025/mcp-server/pyproject.toml +++ b/2025/mcp-server/pyproject.toml @@ -3,7 +3,9 @@ name = "mcp-example" version = "0.1.0" requires-python = ">=3.13" dependencies = [ + "fastapi[standard]>=0.115.12", "httpx>=0.28.1", "mcp[cli]>=1.8.1", + "uvicorn>=0.34.2", "youtube-search>=2.1.2", ] diff --git a/2025/mcp-server/uv.lock b/2025/mcp-server/uv.lock index 62b22433..c52c33b7 100644 --- a/2025/mcp-server/uv.lock +++ b/2025/mcp-server/uv.lock @@ -76,6 +76,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753, upload-time = "2024-12-15T14:28:10.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705, upload-time = "2024-12-15T14:28:06.18Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + [[package]] name = "h11" version = "0.16.0" @@ -98,6 +163,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -131,6 +211,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -143,6 +235,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + [[package]] name = "mcp" version = "1.8.1" @@ -174,15 +294,19 @@ name = "mcp-example" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "mcp", extra = ["cli"] }, + { name = "uvicorn" }, { name = "youtube-search" }, ] [package.metadata] requires-dist = [ + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "mcp", extras = ["cli"], specifier = ">=1.8.1" }, + { name = "uvicorn", specifier = ">=0.34.2" }, { name = "youtube-search", specifier = ">=2.1.2" }, ] @@ -279,6 +403,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + [[package]] name = "requests" version = "2.32.3" @@ -307,6 +448,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] +[[package]] +name = "rich-toolkit" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/31/b6d055f291a660a7bcaec4bcc9457b9fef8ecb6293e527b1eef1840aefd4/rich_toolkit-0.14.6.tar.gz", hash = "sha256:9dbd40e83414b84e828bf899115fff8877ce5951b73175f44db142902f07645d", size = 110805, upload-time = "2025-05-12T19:19:15.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3c/7a824c0514e87c61000583ac22c8321da6dc8e58a93d5f56e583482a2ee0/rich_toolkit-0.14.6-py3-none-any.whl", hash = "sha256:764f3a5f9e4b539ce805596863299e8982599514906dc5e3ccc2d390ef74c301", size = 24815, upload-time = "2025-05-12T19:19:13.713Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -408,6 +563,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload-time = "2025-04-08T10:36:26.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531, upload-time = "2025-04-08T10:35:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417, upload-time = "2025-04-08T10:35:37.048Z" }, + { url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423, upload-time = "2025-04-08T10:35:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185, upload-time = "2025-04-08T10:35:39.708Z" }, + { url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696, upload-time = "2025-04-08T10:35:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327, upload-time = "2025-04-08T10:35:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741, upload-time = "2025-04-08T10:35:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995, upload-time = "2025-04-08T10:35:46.336Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693, upload-time = "2025-04-08T10:35:48.161Z" }, + { url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677, upload-time = "2025-04-08T10:35:49.65Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804, upload-time = "2025-04-08T10:35:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload-time = "2025-04-08T10:35:52.458Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "youtube-search" version = "2.1.2" diff --git a/2025/mcp-server/video_api.py b/2025/mcp-server/video_api.py new file mode 100644 index 00000000..54bcdca3 --- /dev/null +++ b/2025/mcp-server/video_api.py @@ -0,0 +1,52 @@ +from typing import Optional + +import uvicorn +from fastapi import FastAPI, Query +from pydantic import BaseModel +from yt_helper import search_youtube + +app = FastAPI(title="YouTube Video Search API") + + +class Video(BaseModel): + title: str + channel: str + duration: str + description: Optional[str] + views: Optional[str] + url: str + publish_time: Optional[str] + + +@app.get("/videos", response_model=list[Video]) +async def get_videos( + search: str = Query(..., description="Search query"), + max_results: int = Query( + 5, ge=1, le=50, description="Max number of videos to return" + ), +): + """Search for YouTube videos.""" + results = search_youtube(search, max_results=max_results) + + formatted = [ + Video( + title=video.get("title", "Unknown"), + channel=video.get("channel", "Unknown"), + duration=video.get("duration", "Unknown"), + description=video.get("description", None), + views=video.get("views", None), + url=f"https://www.youtube.com/watch?v={video.get('id', '')}", + publish_time=video.get("publish_time", None), + ) + for video in results + ] + + return formatted + + +def main(): + uvicorn.run("video_api:app", host="0.0.0.0", port=8000, reload=True) + + +if __name__ == "__main__": + main() diff --git a/2025/mcp-server/video_repository.py b/2025/mcp-server/yt_helper.py similarity index 73% rename from 2025/mcp-server/video_repository.py rename to 2025/mcp-server/yt_helper.py index 21b225b9..6f54cfde 100644 --- a/2025/mcp-server/video_repository.py +++ b/2025/mcp-server/yt_helper.py @@ -3,7 +3,7 @@ from youtube_search import YoutubeSearch -def search_youtube(query: str, max_results: int = 10) -> dict[str, Any]: +def search_youtube(query: str, max_results: int = 10) -> list[dict[str, Any]]: """Search YouTube for a given query and return the results.""" results = YoutubeSearch(query, max_results=max_results).to_dict() return results From 148eca5cfe0d47330578e56fc02dd075da1a5d1c Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 20 May 2025 16:28:25 +0200 Subject: [PATCH 022/113] Updated example. --- 2025/mcp-server/mcp_videos.py | 8 +++++--- 2025/mcp-server/mcp_videos_api.py | 20 +++++++++++--------- 2025/mcp-server/video_api.py | 4 ++-- 2025/mcp-server/yt_helper.py | 5 +++++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/2025/mcp-server/mcp_videos.py b/2025/mcp-server/mcp_videos.py index 3a3c1768..edcb289b 100644 --- a/2025/mcp-server/mcp_videos.py +++ b/2025/mcp-server/mcp_videos.py @@ -1,11 +1,13 @@ +from typing import Any + from mcp.server.fastmcp import FastMCP -from yt_helper import search_youtube +from yt_helper import construct_video_url, search_youtube # Initialize FastMCP server mcp = FastMCP("videos") -def format_video(video: dict[str, str]) -> str: +def format_video(video: dict[str, Any]) -> str: """Format a video feature into a readable string.""" return f""" Title: {video.get("title", "Unknown")} @@ -13,7 +15,7 @@ def format_video(video: dict[str, str]) -> str: Duration: {video.get("duration", "Unknown")} Description: {video.get("description", "No description available")} Views: {video.get("views", "Unknown")} - URL: https://www.youtube.com/watch?v={video.get("id", "Unknown")} + URL: {construct_video_url(video.get("id", "dQw4w9WgXcQ"))} Published: {video.get("publish_time", "Unknown")} """ diff --git a/2025/mcp-server/mcp_videos_api.py b/2025/mcp-server/mcp_videos_api.py index 9fa35425..9df3bf24 100644 --- a/2025/mcp-server/mcp_videos_api.py +++ b/2025/mcp-server/mcp_videos_api.py @@ -1,3 +1,5 @@ +from typing import Any + import httpx from mcp.server.fastmcp import FastMCP @@ -7,17 +9,17 @@ API_URL = "http://localhost:8000/videos" # Adjust if hosted elsewhere -def format_video(video: dict[str, str]) -> str: +def format_video(video: dict[str, Any]) -> str: """Format a video feature into a readable string.""" return f""" -Title: {video.get("title", "Unknown")} -Channel: {video.get("channel", "Unknown")} -Duration: {video.get("duration", "Unknown")} -Description: {video.get("description", "No description available")} -Views: {video.get("views", "Unknown")} -URL: {video.get("url", "Unknown")} -Published: {video.get("publish_time", "Unknown")} -""".strip() + Title: {video.get("title", "Unknown")} + Channel: {video.get("channel", "Unknown")} + Duration: {video.get("duration", "Unknown")} + Description: {video.get("description", "No description available")} + Views: {video.get("views", "Unknown")} + URL: {video.get("url", "Unknown")} + Published: {video.get("publish_time", "Unknown")} + """ @mcp.tool() diff --git a/2025/mcp-server/video_api.py b/2025/mcp-server/video_api.py index 54bcdca3..a1893dd7 100644 --- a/2025/mcp-server/video_api.py +++ b/2025/mcp-server/video_api.py @@ -3,7 +3,7 @@ import uvicorn from fastapi import FastAPI, Query from pydantic import BaseModel -from yt_helper import search_youtube +from yt_helper import construct_video_url, search_youtube app = FastAPI(title="YouTube Video Search API") @@ -35,7 +35,7 @@ async def get_videos( duration=video.get("duration", "Unknown"), description=video.get("description", None), views=video.get("views", None), - url=f"https://www.youtube.com/watch?v={video.get('id', '')}", + url=construct_video_url(video.get("id", "dQw4w9WgXcQ")), publish_time=video.get("publish_time", None), ) for video in results diff --git a/2025/mcp-server/yt_helper.py b/2025/mcp-server/yt_helper.py index 6f54cfde..ef1f91b0 100644 --- a/2025/mcp-server/yt_helper.py +++ b/2025/mcp-server/yt_helper.py @@ -7,3 +7,8 @@ def search_youtube(query: str, max_results: int = 10) -> list[dict[str, Any]]: """Search YouTube for a given query and return the results.""" results = YoutubeSearch(query, max_results=max_results).to_dict() return results + + +def construct_video_url(video_id: str) -> str: + """Construct a YouTube video URL from the video ID.""" + return f"https://www.youtube.com/watch?v={video_id}" From 81fd49f0065442673fbd5f97529af68f4990b365 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 May 2025 13:21:13 +0200 Subject: [PATCH 023/113] Added dataclasses examples. --- 2025/dataclasses/books.db | Bin 0 -> 12288 bytes 2025/dataclasses/fastapi_example.py | 44 ++++ 2025/dataclasses/fastapi_model_example.py | 70 ++++++ 2025/dataclasses/fastapi_model_example_v2.py | 74 ++++++ 2025/dataclasses/fastapi_sqlalchemy.py | 105 ++++++++ 2025/dataclasses/pydantic_dc.py | 45 ++++ 2025/dataclasses/pyproject.toml | 10 + 2025/dataclasses/uv.lock | 241 +++++++++++++++++++ 8 files changed, 589 insertions(+) create mode 100644 2025/dataclasses/books.db create mode 100644 2025/dataclasses/fastapi_example.py create mode 100644 2025/dataclasses/fastapi_model_example.py create mode 100644 2025/dataclasses/fastapi_model_example_v2.py create mode 100644 2025/dataclasses/fastapi_sqlalchemy.py create mode 100644 2025/dataclasses/pydantic_dc.py create mode 100644 2025/dataclasses/pyproject.toml create mode 100644 2025/dataclasses/uv.lock diff --git a/2025/dataclasses/books.db b/2025/dataclasses/books.db new file mode 100644 index 0000000000000000000000000000000000000000..c8cde0fe64b4138036ce2e2d414ab656f20fe7c5 GIT binary patch literal 12288 zcmeI$ze~eF6bJCT)L#vhxJU+TpYrQg?03v&pkg9GlW5DyZ4Or)$ftH!YXi9go^qjx)RXO`QgQ@cxrz&BLGP}cOC&RKBpi-T$eXJCF0OqsIS>$l00bZa0SG_<0uX=z1Rwwb P2rRL{cBYp7>zBX}A(maa literal 0 HcmV?d00001 diff --git a/2025/dataclasses/fastapi_example.py b/2025/dataclasses/fastapi_example.py new file mode 100644 index 00000000..91e33b6c --- /dev/null +++ b/2025/dataclasses/fastapi_example.py @@ -0,0 +1,44 @@ +import secrets + +import uvicorn +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI() + + +class Book(BaseModel): + title: str + author: str + pages: int = 0 + + +books_db: dict[str, Book] = {} + + +def generate_id() -> str: + # Generate a 24-character hex string (like MongoDB ObjectID) + return secrets.token_hex(12) + + +@app.post("/books/") +def create_book(book: Book): + book_id = generate_id() + books_db[book_id] = book + return {"id": book_id, "book": book} + + +@app.get("/books/{book_id}") +def get_book(book_id: str): + if book_id not in books_db: + raise HTTPException(status_code=404, detail="Book not found") + return {"id": book_id, "book": books_db[book_id]} + + +@app.get("/books/") +def list_books(): + return [{"id": book_id, "book": book} for book_id, book in books_db.items()] + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/dataclasses/fastapi_model_example.py b/2025/dataclasses/fastapi_model_example.py new file mode 100644 index 00000000..a1072663 --- /dev/null +++ b/2025/dataclasses/fastapi_model_example.py @@ -0,0 +1,70 @@ +import secrets +from dataclasses import dataclass, field + +import uvicorn +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI() + +# --- DOMAIN MODEL --- + + +def generate_id() -> str: + return secrets.token_hex(12) + + +@dataclass +class Book: + title: str + author: str + pages: int = 0 + id: str = field(default_factory=generate_id) + + +# --- Pydantic MODELS for API --- + + +class BookCreate(BaseModel): + title: str + author: str + pages: int = 0 + + +class BookResponse(BaseModel): + id: str + title: str + author: str + pages: int + + +# --- FAKE DB --- + +books_db: dict[str, Book] = {} + + +# --- ROUTES --- + + +@app.post("/books/", response_model=BookResponse) +def create_book(book_data: BookCreate): + book = Book(**book_data.dict()) # ID is generated automatically + books_db[book.id] = book + return BookResponse(**book.__dict__) + + +@app.get("/books/{book_id}", response_model=BookResponse) +def get_book(book_id: str): + book = books_db.get(book_id) + if not book: + raise HTTPException(status_code=404, detail="Book not found") + return BookResponse(**book.__dict__) + + +@app.get("/books/", response_model=list[BookResponse]) +def list_books(): + return [BookResponse(**book.__dict__) for book in books_db.values()] + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/dataclasses/fastapi_model_example_v2.py b/2025/dataclasses/fastapi_model_example_v2.py new file mode 100644 index 00000000..a029e627 --- /dev/null +++ b/2025/dataclasses/fastapi_model_example_v2.py @@ -0,0 +1,74 @@ +import secrets + +import uvicorn +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, dataclasses + +app = FastAPI() + +# --- DOMAIN MODEL (Pydantic Dataclass) --- + + +def generate_id() -> str: + return secrets.token_hex(12) + + +@dataclasses.dataclass +class Book: + title: str + author: str + pages: int = 0 + id: str = dataclasses.Field(default_factory=generate_id) + + +# --- API MODELS (Pydantic BaseModels) --- + + +class BookCreate(BaseModel): + title: str + author: str + pages: int = 0 + + class Config: + from_attributes = True + + +class BookResponse(BaseModel): + id: str + title: str + author: str + pages: int + + class Config: + from_attributes = True + + +# --- FAKE IN-MEMORY DB --- + +books_db: dict[str, Book] = {} + +# --- ROUTES --- + + +@app.post("/books/", response_model=BookResponse) +def create_book(book_data: BookCreate): + book = Book(**book_data.model_dump()) + books_db[book.id] = book + return BookResponse.model_validate(book) + + +@app.get("/books/{book_id}", response_model=BookResponse) +def get_book(book_id: str): + book = books_db.get(book_id) + if not book: + raise HTTPException(status_code=404, detail="Book not found") + return BookResponse.model_validate(book) + + +@app.get("/books/", response_model=list[BookResponse]) +def list_books(): + return [BookResponse.model_validate(book) for book in books_db.values()] + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/dataclasses/fastapi_sqlalchemy.py b/2025/dataclasses/fastapi_sqlalchemy.py new file mode 100644 index 00000000..0f004192 --- /dev/null +++ b/2025/dataclasses/fastapi_sqlalchemy.py @@ -0,0 +1,105 @@ +import secrets + +import uvicorn +from fastapi import Depends, FastAPI, HTTPException +from pydantic import BaseModel +from sqlalchemy import Integer, String, create_engine +from sqlalchemy.orm import ( + DeclarativeBase, + Mapped, + Session, + mapped_column, + sessionmaker, +) + +# --- Database setup --- + +DATABASE_URL = "sqlite:///./books.db" +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) + +SessionLocal = sessionmaker(bind=engine, autoflush=False) + + +# declarative base class +class Base(DeclarativeBase): + pass + + +# --- SQLAlchemy ORM model --- + + +class Book(Base): + __tablename__ = "books" + + id: Mapped[str] = mapped_column( + primary_key=True, default=lambda: secrets.token_hex(12) + ) + title: Mapped[str] = mapped_column(String, nullable=False) + author: Mapped[str] = mapped_column(String, nullable=False) + pages: Mapped[int] = mapped_column(Integer, default=0) + + +# --- Pydantic models --- + + +class BookCreate(BaseModel): + title: str + author: str + pages: int = 0 + + +class BookResponse(BaseModel): + id: str + title: str + author: str + pages: int + + class Config: + from_attributes = True # allows conversion from ORM objects + + +# --- Dependency to get a DB session --- + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# --- FastAPI setup --- + +app = FastAPI() + +# Create tables +Base.metadata.create_all(bind=engine) + +# --- Routes --- + + +@app.post("/books/", response_model=BookResponse) +def create_book(book_data: BookCreate, db: Session = Depends(get_db)): + book = Book(**book_data.model_dump()) + db.add(book) + db.commit() + db.refresh(book) + return book + + +@app.get("/books/{book_id}", response_model=BookResponse) +def get_book(book_id: str, db: Session = Depends(get_db)): + book = db.get(Book, book_id) + if not book: + raise HTTPException(status_code=404, detail="Book not found") + return book + + +@app.get("/books/", response_model=list[BookResponse]) +def list_books(db: Session = Depends(get_db)): + return db.query(Book).all() + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/dataclasses/pydantic_dc.py b/2025/dataclasses/pydantic_dc.py new file mode 100644 index 00000000..a12bfc19 --- /dev/null +++ b/2025/dataclasses/pydantic_dc.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, ValidationError, validator +from pydantic.dataclasses import dataclass + + +@dataclass +class Book: + title: str + pages: int + + @validator("pages") + def pages_must_be_positive(cls, v: int): + if v < 0: + raise ValueError("Pages must be a positive integer") + return v + + +class Author(BaseModel): + name: str + age: int + + @validator("age") + def age_must_be_positive(cls, v: int): + if v < 0: + raise ValueError("Age must be a positive integer") + return v + + +def main() -> None: + # Valid input + book = Book(title="1984", pages=328) # pages will be converted to int + + # print(book.model_dump()) # {'title': '1984', 'pages': 328} + + # Invalid input โ€“ will raise a ValidationError + try: + bad_book = Book(title="The Hobbit", pages="three hundred") + except ValidationError as e: + print(e) + + author = Author(name="J.R.R. Tolkien", age=81) + print(author.model_dump()) # {'name': 'J.R.R. Tolkien', 'age': 81} + + +if __name__ == "__main__": + main() diff --git a/2025/dataclasses/pyproject.toml b/2025/dataclasses/pyproject.toml new file mode 100644 index 00000000..4266fb1b --- /dev/null +++ b/2025/dataclasses/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "dataclasses_vs_pydantic" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115.12", + "pydantic>=2.11.4", + "sqlalchemy>=2.0.41", + "uvicorn>=0.34.2", +] diff --git a/2025/dataclasses/uv.lock b/2025/dataclasses/uv.lock new file mode 100644 index 00000000..bf324b5b --- /dev/null +++ b/2025/dataclasses/uv.lock @@ -0,0 +1,241 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dataclasses-vs-pydantic" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "pydantic", specifier = ">=2.11.4" }, + { name = "sqlalchemy", specifier = ">=2.0.41" }, + { name = "uvicorn", specifier = ">=0.34.2" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797, upload-time = "2025-05-09T19:47:35.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150, upload-time = "2025-05-09T14:50:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381, upload-time = "2025-05-09T15:24:12.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427, upload-time = "2025-05-09T15:24:51.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795, upload-time = "2025-05-09T15:29:26.673Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398, upload-time = "2025-05-09T14:53:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795, upload-time = "2025-05-09T14:53:47.039Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976, upload-time = "2025-05-09T15:27:06.542Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509, upload-time = "2025-05-09T14:54:02.223Z" }, + { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023, upload-time = "2025-05-09T14:53:24.157Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911, upload-time = "2025-05-09T15:24:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251, upload-time = "2025-05-09T15:24:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620, upload-time = "2025-05-09T15:29:28.051Z" }, + { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851, upload-time = "2025-05-09T14:53:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718, upload-time = "2025-05-09T14:53:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752, upload-time = "2025-05-09T15:27:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170, upload-time = "2025-05-09T14:54:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899, upload-time = "2025-05-09T14:54:01.581Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, +] From d09054072c6cb8a6c2dd08c630fb3a131f19fdc9 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 24 Jun 2025 16:48:18 +0200 Subject: [PATCH 024/113] Added library examples. --- 2025/libraries/fastapi_page_example.py | 37 + 2025/libraries/flet_example.py | 8 + 2025/libraries/httpx_example.py | 6 + 2025/libraries/hypothesis_example.py | 7 + 2025/libraries/nicegui_example.py | 5 + 2025/libraries/pydantic_ai_example.py | 12 + 2025/libraries/pyproject.toml | 21 + 2025/libraries/pyset_example.py | 14 + 2025/libraries/rich_example.py | 3 + 2025/libraries/settings.env | 2 + 2025/libraries/tabulate_example.py | 4 + 2025/libraries/textual_example.py | 10 + 2025/libraries/toolz_example.py | 4 + 2025/libraries/uv.lock | 2252 ++++++++++++++++++++++++ 14 files changed, 2385 insertions(+) create mode 100644 2025/libraries/fastapi_page_example.py create mode 100644 2025/libraries/flet_example.py create mode 100644 2025/libraries/httpx_example.py create mode 100644 2025/libraries/hypothesis_example.py create mode 100644 2025/libraries/nicegui_example.py create mode 100644 2025/libraries/pydantic_ai_example.py create mode 100644 2025/libraries/pyproject.toml create mode 100644 2025/libraries/pyset_example.py create mode 100644 2025/libraries/rich_example.py create mode 100644 2025/libraries/settings.env create mode 100644 2025/libraries/tabulate_example.py create mode 100644 2025/libraries/textual_example.py create mode 100644 2025/libraries/toolz_example.py create mode 100644 2025/libraries/uv.lock diff --git a/2025/libraries/fastapi_page_example.py b/2025/libraries/fastapi_page_example.py new file mode 100644 index 00000000..5765a138 --- /dev/null +++ b/2025/libraries/fastapi_page_example.py @@ -0,0 +1,37 @@ +import uvicorn +from fastapi import FastAPI +from fastapi_pagination import Page, add_pagination, paginate +from pydantic import BaseModel + +app = FastAPI() + + +# --- Define your Pydantic model --- +class Item(BaseModel): + id: int + name: str + + +# --- Fake database --- +items_db: list[Item] = [ + Item(id=i, name=f"Item {i}") for i in range(1, 101) +] # 100 items + + +# --- Paginated endpoint --- +@app.get("/items", response_model=Page[Item]) +def get_items(): + return paginate(items_db) + + +# --- Activate pagination --- +add_pagination(app) + + +def main() -> None: + uvicorn.run("fastapi_page_example:app", host="127.0.0.1", port=8000, reload=True) + + +# --- Run server directly --- +if __name__ == "__main__": + main() diff --git a/2025/libraries/flet_example.py b/2025/libraries/flet_example.py new file mode 100644 index 00000000..04c414c9 --- /dev/null +++ b/2025/libraries/flet_example.py @@ -0,0 +1,8 @@ +import flet as ft + + +def main(page: ft.Page): + page.add(ft.Text("Hello, Flet!")) + + +ft.app(target=main) diff --git a/2025/libraries/httpx_example.py b/2025/libraries/httpx_example.py new file mode 100644 index 00000000..789b3c17 --- /dev/null +++ b/2025/libraries/httpx_example.py @@ -0,0 +1,6 @@ +import httpx + +response = httpx.get( + "https://business.arjancodes.com/api/v0/courses/66951fb842a33dd06c85e343" +) +print(response.json()) diff --git a/2025/libraries/hypothesis_example.py b/2025/libraries/hypothesis_example.py new file mode 100644 index 00000000..bcccb395 --- /dev/null +++ b/2025/libraries/hypothesis_example.py @@ -0,0 +1,7 @@ +from hypothesis import given +from hypothesis.strategies import integers, lists + + +@given(lists(integers())) +def test_sort_idempotent(xs: list[int]) -> None: + assert sorted(sorted(xs)) == sorted(xs) diff --git a/2025/libraries/nicegui_example.py b/2025/libraries/nicegui_example.py new file mode 100644 index 00000000..b03f15e3 --- /dev/null +++ b/2025/libraries/nicegui_example.py @@ -0,0 +1,5 @@ +from nicegui import ui + +ui.label("Hello from NiceGUI!") +ui.button("Click me", on_click=lambda: ui.notify("Clicked!")) +ui.run() diff --git a/2025/libraries/pydantic_ai_example.py b/2025/libraries/pydantic_ai_example.py new file mode 100644 index 00000000..a759392a --- /dev/null +++ b/2025/libraries/pydantic_ai_example.py @@ -0,0 +1,12 @@ +from pydantic_ai import Agent + +agent = Agent( + "google-gla:gemini-1.5-flash", + system_prompt="Be concise, reply with one sentence.", +) + +result = agent.run_sync('Where does "hello world" come from?') +print(result.output) +""" +The first known use of "hello, world" was in a 1974 textbook about the C programming language. +""" diff --git a/2025/libraries/pyproject.toml b/2025/libraries/pyproject.toml new file mode 100644 index 00000000..7d6a45bf --- /dev/null +++ b/2025/libraries/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "libraries" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115.13", + "fastapi-pagination>=0.13.2", + "flet[all]>=0.28.3", + "httpx>=0.28.1", + "hypothesis>=6.135.14", + "nicegui>=2.20.0", + "pydantic-ai>=0.3.2", + "pydantic-settings>=2.10.0", + "pytest>=8.4.1", + "reflex>=0.7.14", + "rich>=14.0.0", + "tabulate>=0.9.0", + "textual>=3.5.0", + "toolz>=1.0.0", + "uvicorn>=0.34.3", +] diff --git a/2025/libraries/pyset_example.py b/2025/libraries/pyset_example.py new file mode 100644 index 00000000..f73ee683 --- /dev/null +++ b/2025/libraries/pyset_example.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + database_url: str + debug: bool = False + + class Config: + env_file = "settings.env" + + +settings = Settings() +print(f"Database URL: {settings.database_url}") +print(f"Debug mode: {settings.debug}") diff --git a/2025/libraries/rich_example.py b/2025/libraries/rich_example.py new file mode 100644 index 00000000..25c45c85 --- /dev/null +++ b/2025/libraries/rich_example.py @@ -0,0 +1,3 @@ +from rich import print + +print("[bold magenta]Hello, World![/bold magenta]") diff --git a/2025/libraries/settings.env b/2025/libraries/settings.env new file mode 100644 index 00000000..c2d733c7 --- /dev/null +++ b/2025/libraries/settings.env @@ -0,0 +1,2 @@ +DATABASE_URL=postgresql://user:pass@localhost:5432/mydb +DEBUG=true \ No newline at end of file diff --git a/2025/libraries/tabulate_example.py b/2025/libraries/tabulate_example.py new file mode 100644 index 00000000..acf8eb18 --- /dev/null +++ b/2025/libraries/tabulate_example.py @@ -0,0 +1,4 @@ +from tabulate import tabulate + +table = [["Alice", 24], ["Bob", 19]] +print(tabulate(table, headers=["Name", "Age"])) diff --git a/2025/libraries/textual_example.py b/2025/libraries/textual_example.py new file mode 100644 index 00000000..09f2148e --- /dev/null +++ b/2025/libraries/textual_example.py @@ -0,0 +1,10 @@ +from textual.app import App +from textual.widgets import Static + + +class MyApp(App): + def compose(self): + yield Static("Hello from Textual!") + + +MyApp().run() diff --git a/2025/libraries/toolz_example.py b/2025/libraries/toolz_example.py new file mode 100644 index 00000000..47328dd0 --- /dev/null +++ b/2025/libraries/toolz_example.py @@ -0,0 +1,4 @@ +from toolz import compose + +strip_upper = compose(str.strip, str.upper) +print(strip_upper(" hello ")) # -> "HELLO" diff --git a/2025/libraries/uv.lock b/2025/libraries/uv.lock new file mode 100644 index 00000000..5cd90cb0 --- /dev/null +++ b/2025/libraries/uv.lock @@ -0,0 +1,2252 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload-time = "2025-06-14T15:14:30.604Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload-time = "2025-06-14T15:14:32.275Z" }, + { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload-time = "2025-06-14T15:14:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload-time = "2025-06-14T15:14:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload-time = "2025-06-14T15:14:38Z" }, + { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload-time = "2025-06-14T15:14:39.951Z" }, + { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload-time = "2025-06-14T15:14:42.151Z" }, + { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload-time = "2025-06-14T15:14:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload-time = "2025-06-14T15:14:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload-time = "2025-06-14T15:14:47.911Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload-time = "2025-06-14T15:14:50.334Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload-time = "2025-06-14T15:14:52.378Z" }, + { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload-time = "2025-06-14T15:14:54.617Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload-time = "2025-06-14T15:14:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload-time = "2025-06-14T15:14:58.598Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload-time = "2025-06-14T15:15:00.939Z" }, + { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload-time = "2025-06-14T15:15:02.858Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, +] + +[[package]] +name = "alembic" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/35/116797ff14635e496bbda0c168987f5326a6555b09312e9b817e360d1f56/alembic-1.16.2.tar.gz", hash = "sha256:e53c38ff88dadb92eb22f8b150708367db731d58ad7e9d417c9168ab516cbed8", size = 1963563, upload-time = "2025-06-16T18:05:08.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl", hash = "sha256:5f42e9bd0afdbd1d5e3ad856c01754530367debdebf21ed6894e34af52b3bb03", size = 242717, upload-time = "2025-06-16T18:05:10.27Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/19/e2e09bc7fc0c4562ae865b3e5d487931c254c517e1c739b0c8aef2cf3186/anthropic-0.55.0.tar.gz", hash = "sha256:61826efa1bda0e4c7dc6f6a0d82b7d99b3fda970cd048d40ef5fca08a5eabd33", size = 408192, upload-time = "2025-06-23T18:52:26.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/8f/ba982f539db40f49a610f61562e9b54fb9c85e7b9ede9a46ff6f9e79042f/anthropic-0.55.0-py3-none-any.whl", hash = "sha256:3518433fc0372a13f2b793b4cabecc7734ec9176e063a0f28dac19aa17c57f94", size = 289318, upload-time = "2025-06-23T18:52:24.478Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + +[[package]] +name = "binaryornot" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/fe/7ebfec74d49f97fc55cd38240c7a7d08134002b1e14be8c3897c0dd5e49b/binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", size = 371054, upload-time = "2017-08-03T15:55:25.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006, upload-time = "2017-08-03T15:55:31.23Z" }, +] + +[[package]] +name = "boto3" +version = "1.38.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/d3/43ea4ee898b3d0b3865b8529d9699d46876687c14b470081e2d7692531a5/boto3-1.38.42.tar.gz", hash = "sha256:2cb783c668ae4f2a86b6497b47251b9baf9a16db8fff863b57eae683276b9e1f", size = 111838, upload-time = "2025-06-23T19:27:04.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/a9/13160d0e6b44c7fa9d6805a8cc83303af32455140b1b7decd8ff23fbbfee/boto3-1.38.42-py3-none-any.whl", hash = "sha256:a9b4c7021bf5adee985523fc87db27a7200de161c094cb8f709b93a81797dc8a", size = 139923, upload-time = "2025-06-23T19:27:03.145Z" }, +] + +[[package]] +name = "botocore" +version = "1.38.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/4d/4b543efe1ce84791adc7fdd33e397d29c958467ba04f06f0c23c90343e69/botocore-1.38.42.tar.gz", hash = "sha256:3a14188e48f6e26be561164373d34150fa9cb39f7ad32cc745dcd3ab05f43683", size = 14039614, upload-time = "2025-06-23T19:26:55.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/7e/250b9c8651c85becb3f6646101299e5519a209671f8bd9f0d4c16a12c629/botocore-1.38.42-py3-none-any.whl", hash = "sha256:fbbeac30c045b5c19f1c3bb063ea2b6315ce2d6fcb3d898e87d1c1846297961c", size = 13701936, upload-time = "2025-06-23T19:26:50.418Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "cohere" +version = "5.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/33/69c7d1b25a20eafef4197a1444c7f87d5241e936194e54876ea8996157e6/cohere-5.15.0.tar.gz", hash = "sha256:e802d4718ddb0bb655654382ebbce002756a3800faac30296cde7f1bdc6ff2cc", size = 135021, upload-time = "2025-04-15T13:39:51.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/87/94694db7fe6df979fbc03286eaabdfa98f1c8fa532960e5afdf965e10960/cohere-5.15.0-py3-none-any.whl", hash = "sha256:22ff867c2a6f2fc2b585360c6072f584f11f275ef6d9242bac24e0fa2df1dfb5", size = 259522, upload-time = "2025-04-15T13:39:49.498Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cookiecutter" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, + { name = "binaryornot" }, + { name = "click" }, + { name = "jinja2" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/17/9f2cd228eb949a91915acd38d3eecdc9d8893dde353b603f0db7e9f6be55/cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c", size = 158767, upload-time = "2024-02-21T18:02:41.949Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177, upload-time = "2024-02-21T18:02:39.569Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, +] + +[[package]] +name = "fasta2a" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/21/c79bd6082ce107275449d180d49af9248068bb2f10375666f963a418e20c/fasta2a-0.3.2.tar.gz", hash = "sha256:cfb8f6d4a7e72f4c23f57c08476563889efbd64218cdb0dbb051faeca53f5989", size = 12292, upload-time = "2025-06-21T05:25:08.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/e8/b7f49806f697f8f2d2556ae6e6eb7573055c06e9e6e87dfbc618e36fcd61/fasta2a-0.3.2-py3-none-any.whl", hash = "sha256:da5b442d2559b2f4bb44807c997139ba15e22ba74f5790181f568be9a75d833b", size = 15328, upload-time = "2025-06-21T05:24:58.737Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" }, +] + +[[package]] +name = "fastapi-pagination" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/25/fc3c9ed9d99df279acdbf57da1a3a91ffceae9c087882cbe8a9cb1229b1e/fastapi_pagination-0.13.2.tar.gz", hash = "sha256:5e76f129aef706601b86114428ca3ff68715bfa3929bf4df8c7ed27561d7f661", size = 550389, upload-time = "2025-06-07T09:30:44.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/cb/cf2f10d4620b31a77705226c7292f39b4a191cef3485ea42561fc2e157d9/fastapi_pagination-0.13.2-py3-none-any.whl", hash = "sha256:d2ec66ffda5cd9c1d665521f3916b16ebbb15d5010a945449292540ef70c4d9a", size = 50404, upload-time = "2025-06-07T09:30:42.218Z" }, +] + +[[package]] +name = "fastavro" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/8f/32664a3245247b13702d13d2657ea534daf64e58a3f72a3a2d10598d6916/fastavro-1.11.1.tar.gz", hash = "sha256:bf6acde5ee633a29fb8dfd6dfea13b164722bc3adc05a0e055df080549c1c2f8", size = 1016250, upload-time = "2025-05-18T04:54:31.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/08/8e25b9e87a98f8c96b25e64565fa1a1208c0095bb6a84a5c8a4b925688a5/fastavro-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f963b8ddaf179660e814ab420850c1b4ea33e2ad2de8011549d958b21f77f20a", size = 931520, upload-time = "2025-05-18T04:55:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/7cf5561ef94781ed6942cee6b394a5e698080f4247f00f158ee396ec244d/fastavro-1.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0253e5b6a3c9b62fae9fc3abd8184c5b64a833322b6af7d666d3db266ad879b5", size = 3195989, upload-time = "2025-05-18T04:55:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/f02f097d79f090e5c5aca8a743010c4e833a257c0efdeb289c68294f7928/fastavro-1.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca637b150e1f4c0e8e564fad40a16bd922bcb7ffd1a6e4836e6084f2c4f4e8db", size = 3239755, upload-time = "2025-05-18T04:55:16.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/4c/46626b4ee4eb8eb5aa7835973c6ba8890cf082ef2daface6071e788d2992/fastavro-1.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76af1709031621828ca6ce7f027f7711fa33ac23e8269e7a5733996ff8d318da", size = 3243788, upload-time = "2025-05-18T04:55:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6f/8ed42524e9e8dc0554f0f211dd1c6c7a9dde83b95388ddcf7c137e70796f/fastavro-1.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8224e6d8d9864d4e55dafbe88920d6a1b8c19cc3006acfac6aa4f494a6af3450", size = 3378330, upload-time = "2025-05-18T04:55:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/b8/51/38cbe243d5facccab40fc43a4c17db264c261be955ce003803d25f0da2c3/fastavro-1.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:cde7ed91b52ff21f0f9f157329760ba7251508ca3e9618af3ffdac986d9faaa2", size = 443115, upload-time = "2025-05-18T04:55:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/d0/57/0d31ed1a49c65ad9f0f0128d9a928972878017781f9d4336f5f60982334c/fastavro-1.11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e5ed1325c1c414dd954e7a2c5074daefe1eceb672b8c727aa030ba327aa00693", size = 1021401, upload-time = "2025-05-18T04:55:23.431Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/a3f1a75fbfc16b3eff65dc0efcdb92364967923194312b3f8c8fc2cb95be/fastavro-1.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd3c95baeec37188899824faf44a5ee94dfc4d8667b05b2f867070c7eb174c4", size = 3384349, upload-time = "2025-05-18T04:55:25.575Z" }, + { url = "https://files.pythonhosted.org/packages/be/84/02bceb7518867df84027232a75225db758b9b45f12017c9743f45b73101e/fastavro-1.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e0babcd81acceb4c60110af9efa25d890dbb68f7de880f806dadeb1e70fe413", size = 3240658, upload-time = "2025-05-18T04:55:27.633Z" }, + { url = "https://files.pythonhosted.org/packages/f2/17/508c846c644d39bc432b027112068b8e96e7560468304d4c0757539dd73a/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c0cb8063c7208b53b6867983dc6ae7cc80b91116b51d435d2610a5db2fc52f", size = 3372809, upload-time = "2025-05-18T04:55:30.063Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/9c2917a70ed570ddbfd1d32ac23200c1d011e36c332e59950d2f6d204941/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1bc2824e9969c04ab6263d269a1e0e5d40b9bd16ade6b70c29d6ffbc4f3cc102", size = 3387171, upload-time = "2025-05-18T04:55:32.531Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "flet" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "platform_system != 'Pyodide'" }, + { name = "oauthlib", marker = "platform_system != 'Pyodide'" }, + { name = "repath" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/d0/9ba4ee34972e9e0cf54b1f7d17c695491632421f81301993f2aec8d12105/flet-0.28.3-py3-none-any.whl", hash = "sha256:649bfc4af7933956ecf44963df6c0d997bff9ceeaf89d3c86d96803840cab83e", size = 463000, upload-time = "2025-05-20T19:44:58.651Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "flet-cli" }, + { name = "flet-desktop", marker = "sys_platform == 'darwin' or sys_platform == 'win32'" }, + { name = "flet-desktop-light", marker = "sys_platform == 'linux'" }, + { name = "flet-web" }, +] + +[[package]] +name = "flet-cli" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cookiecutter" }, + { name = "flet" }, + { name = "packaging" }, + { name = "qrcode" }, + { name = "toml" }, + { name = "watchdog" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/33/9398add46c07a8247a28dc05fd96e26d12d07c7153126ce67ed42a6439bb/flet_cli-0.28.3-py3-none-any.whl", hash = "sha256:2759e4526472a32a584836cf926a9c3ba8fe6e363b4305454a759668d4fcad70", size = 44149, upload-time = "2025-05-20T19:44:57.336Z" }, +] + +[[package]] +name = "flet-desktop" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/0e/f797d3052de953fa42b804b6ff170ad5c7708fc2959ffcce9d2a49a4b56c/flet_desktop-0.28.3.tar.gz", hash = "sha256:3e5db7b152de8cd3935e98eb39c162bf5c66f595d30c006f28d48d60e77a463d", size = 39876159, upload-time = "2025-05-20T19:39:28.548Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e6/280350788df36041b825a1e2ceeb2a60672f208164f3f2ff75c2cc16f1c8/flet_desktop-0.28.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:6763b8e14863b0ee93e310720efa02202acae6d6ce8ff783663380f350f4f382", size = 47048073, upload-time = "2025-05-20T19:42:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/34/e0/7a1486d8f71bca34ae928f5f8833d19ec0bda5ad5975e8db9e0543ae577b/flet_desktop-0.28.3-py3-none-macosx_12_0_arm64.whl", hash = "sha256:89797a387e743808733f308c7faa1158ab768966180c0fc4207f3e95d3b25db3", size = 47048072, upload-time = "2025-05-20T19:42:41.787Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5f/85e74518b6ef0cebc6f7b1dfc63b523df821602745cf3a31a9607be18cf5/flet_desktop-0.28.3-py3-none-win32.whl", hash = "sha256:dc24f57ba725b974b4795b46e35f2b5348c4843f5117e9fc18b25c4abfa5caf4", size = 40265313, upload-time = "2025-05-20T19:39:19.761Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/c953ee675f2751f14329e23dbf538bb703c64ca1ad1c88e244ca17fe459e/flet_desktop-0.28.3-py3-none-win_amd64.whl", hash = "sha256:35db313302fd4c376ba9be4d43f953a5c67d1ba99180dc6afee702699ed14749", size = 40265319, upload-time = "2025-05-20T19:39:24.527Z" }, +] + +[[package]] +name = "flet-desktop-light" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flet" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/a9/3eb542246b49c40d39ba667c53f1f1f0c40f87b6b96d5542f29e2ae98eb3/flet_desktop_light-0.28.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af6e53224bedd5c287c54ec3021ef3879abbba1265261a01e80badc3fbaddb86", size = 14978766, upload-time = "2025-05-20T19:31:54.681Z" }, + { url = "https://files.pythonhosted.org/packages/c6/06/a21078e117408519e4ea27f18d3826cf067d069d92e684a683261da1ab54/flet_desktop_light-0.28.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f3dc473aae8d6cf4270e59bbb51e819068c343a49a8836cced2d17fb0359a7f5", size = 15579831, upload-time = "2025-05-20T19:32:11.698Z" }, + { url = "https://files.pythonhosted.org/packages/c0/83/195152cc264b37426c8595418d52d51a30ef55d55db9e1c3c22818c01e47/flet_desktop_light-0.28.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d45647d68ce0aaf5418c938e8e02f404bbb8814ece504f7242730520c3697c47", size = 14978756, upload-time = "2025-05-20T19:31:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/07/03/6b44479989b55994c14a88c41b1bdc0635ee19509453d301b7caaff8e1af/flet_desktop_light-0.28.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3090ca7c07392c76bd47ef29f79744c7d114edda466be5f675abc5f65f1f5be7", size = 15579821, upload-time = "2025-05-20T19:32:13.836Z" }, +] + +[[package]] +name = "flet-web" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "flet" }, + { name = "uvicorn", extra = ["standard"] }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/8a/01f73ae7123090b2974f0c5ba70d7d9fc463f47f6e12daeca59fdc40e1a9/flet_web-0.28.3-py3-none-any.whl", hash = "sha256:919c13f374e7cee539d29a8ccd6decb739528ef257c88e60af4df1ebab045bfb", size = 3137744, upload-time = "2025-05-20T19:27:32.443Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "google-genai" +version = "1.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "google-auth" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/8a/4a628e2e918f8c4a48ea778b68395b97b05b6873c6e528e78ccfb02a2c8d/google_genai-1.21.1.tar.gz", hash = "sha256:5412fde7f0b39574a4670a9a25e398824a12b3cddd632fdff66d1b9bcfdbfcb4", size = 205636, upload-time = "2025-06-19T14:09:20.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/5c/659c2b992d631a873ae8fed612ce92af423fdc5f7d541dec7ce8f4b1789e/google_genai-1.21.1-py3-none-any.whl", hash = "sha256:fa6fa5311f9a757ce65cd528a938a0f309bb3032516015bf5b3022e63b2fc46b", size = 206388, upload-time = "2025-06-19T14:09:19.016Z" }, +] + +[[package]] +name = "granian" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/83/3c58fbccda2c70c19a09c26c5559314ac71f258e42d9e57cb88e7e36afa5/granian-2.3.4.tar.gz", hash = "sha256:ed04cb7f9befb1bcb8f97b7fee6d19eb4375346eeddd43c77193c35e9cf66b9a", size = 101156, upload-time = "2025-06-14T13:10:36.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/d5/e04a128a098584008690705f0365290bb8b6d8271d671380e3a185fb0a06/granian-2.3.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f3a7488836f064bb3dedabd09c223bf7408e9fd5bf3a77587c73b51d6ce1dbce", size = 3039582, upload-time = "2025-06-14T13:09:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1f/72f9bbb4e8f1b83d26cfbcfdb4f53711b1a9630b93692e8a0e7fa98588e3/granian-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ba37638c0bfa1b68c852738290ffac99f657ffbc2d6cdcc832d311e189d1947", size = 2721739, upload-time = "2025-06-14T13:09:19.822Z" }, + { url = "https://files.pythonhosted.org/packages/e6/40/c8d79fe2a3900895e595581a9b8be7872e1798efc180f426faf99b73282d/granian-2.3.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29d6d5d407dbd5fcbe58a412619ca060fa3de26e6f37ef48f0ebef2eb8bbec57", size = 3345502, upload-time = "2025-06-14T13:09:21.236Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2b/1dc6537e5ae95fbd553d771590f46bcedb8df3422d7eb034298af6c14d7a/granian-2.3.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90d2c23a8df642af2bac8e40d17b09550d67498b2a8ea6a8ff8844edba9c14e8", size = 2999116, upload-time = "2025-06-14T13:09:22.914Z" }, + { url = "https://files.pythonhosted.org/packages/67/d6/da281d75f7b897182021ce2586ce9f4bcb8e63def0ce905cc8da49122d5a/granian-2.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b94e70080877f368578bc8040936c605eebee204905cd64af0497a4c34307209", size = 3229521, upload-time = "2025-06-14T13:09:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d4/af75b73ee146d4bc673303cb3f9af6f19d66738d0082ec041d9b8b465451/granian-2.3.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a1a393fbb8a3245c0b26ae2cf37649d0f1438d61ea780445885212b878ba4e5d", size = 3136218, upload-time = "2025-06-14T13:09:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/e2089e4d19e674bbe591b69013379004e93d0d05160a9c186726b478fdcb/granian-2.3.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b390427ac03fd09e6fdf5d290f47480b6e1471e2abf3889d278f119cbea97256", size = 3150105, upload-time = "2025-06-14T13:09:27.087Z" }, + { url = "https://files.pythonhosted.org/packages/11/26/917d9ee53ee74471b494ba69f8bb160cbf99200ccf642f67942123033983/granian-2.3.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6135af0ed734b718f1d7ac2540a088971b06ef65db9f58d44c1ce7dd5df354ac", size = 3467642, upload-time = "2025-06-14T13:09:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/50c4911db8cc6f426aa266d0d114741a31a09ad37d10e832d57638f9c088/granian-2.3.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:bac27d8b9bbecd5c2027a23cef6135d83ec05f6d0061b40dfdd9587a3f761579", size = 3267179, upload-time = "2025-06-14T13:09:30.059Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/83e02966e35614a3dbf190307ebec2f45a5f61be2492dd57ae3c56578eb8/granian-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:baca4b613eb9545bc309af213afe1940e4253b22445da667c47f841bb7baa666", size = 2788646, upload-time = "2025-06-14T13:09:31.398Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/6b67b36b9b1b067dc4f956c88a633b821668007b1da393e366e68bb4748f/granian-2.3.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:2224b46ad524d92ab8df5e301c489f2839312b8b487b3dcf8c032ef7f4a9b402", size = 2981091, upload-time = "2025-06-14T13:09:32.644Z" }, + { url = "https://files.pythonhosted.org/packages/65/48/2833a6d0d32eb9f2f1f801ac27ebc4033cffac2789234d27c9fd0ae41e88/granian-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:670ce012d4f6fc045698bdf55b8d7ca262876d4c2fb894aceb5e015b1c488c25", size = 2691018, upload-time = "2025-06-14T13:09:34.513Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/197dd8815eef3249fcc7e9ab67d1cf2312acebcfbdbe0374f0a70d7961d1/granian-2.3.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fedf6fe1c2a4cf2141860d45ff82861bf257c9d71f39e225f1989091d9656c40", size = 3082566, upload-time = "2025-06-14T13:09:36.147Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fd/526a113b20f7d952b2dafe57be0cb10ff3573f00f1ce76c4e97449f0089e/granian-2.3.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:41b020b24ac3888c477a2baf488e39cc64cc1235f6053d4d788542d56acbbb6c", size = 3011576, upload-time = "2025-06-14T13:09:38.277Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/d899269dc1b9daa6707eb18935ef6401e995597bdf0ef04a806fe9f307e9/granian-2.3.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:1fb9132a3535f3d399ed89621580b6eaed6c9df5c3f673e0a6205c987abb26d0", size = 3141072, upload-time = "2025-06-14T13:09:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/d5/95/f9c90d9e61853c31d18f1055e2f7378ed536960aa18c61f352833cd22c9c/granian-2.3.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:c223467643ca1e6436804cc84ec828c6f08c5c24beee6c861f76c9d65b5086c1", size = 3460907, upload-time = "2025-06-14T13:09:41.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/4caddadbf6778fc305e6706120a3cc78738ba421c6ae8ed45ac6ff677332/granian-2.3.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:4b2cef0a948a1278a6c7e5ae1414dbb852f893d56e9574da52e5f89c55fa9c3f", size = 3259581, upload-time = "2025-06-14T13:09:42.646Z" }, + { url = "https://files.pythonhosted.org/packages/00/89/19744b3ca30de038d928019f266812c69f4fb7fb827500d6c03301e6c38c/granian-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:161d7abdc3097669136be543245d687fa23e1c69936b1844f8c5334aadc060ef", size = 2801933, upload-time = "2025-06-14T13:09:44.376Z" }, +] + +[package.optional-dependencies] +reload = [ + { name = "watchfiles" }, +] + +[[package]] +name = "greenlet" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, +] + +[[package]] +name = "groq" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/7d/bb053ba75357bf5e8c33def63fb31c8b0bb86dce07759a0cd8e3232d2df9/groq-0.28.0.tar.gz", hash = "sha256:65e1cab9184cbb32380d62eca50d6162269c7ec0c77e4cc868069cfe93450f9f", size = 131730, upload-time = "2025-06-12T16:22:49.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/24/20fc18d1b3e0883aeb24286ca8f26dc1970561b07d9c4412c84561bdf307/groq-0.28.0-py3-none-any.whl", hash = "sha256:c6f86638371c2cba2ca337232e76c8d412e75965ed7e3058d30c9aa5dfe84303", size = 130217, upload-time = "2025-06-12T16:22:47.97Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d4/7685999e85945ed0d7f0762b686ae7015035390de1161dcea9d5276c134c/hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694", size = 495969, upload-time = "2025-06-20T21:48:38.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/89/a1119eebe2836cb25758e7661d6410d3eae982e2b5e974bcc4d250be9012/hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23", size = 2687929, upload-time = "2025-06-20T21:48:32.284Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/2c78e28f309396e71ec8e4e9304a6483dcbc36172b5cea8f291994163425/hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8", size = 2556338, upload-time = "2025-06-20T21:48:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2f/6cad7b5fe86b7652579346cb7f85156c11761df26435651cbba89376cd2c/hf_xet-1.1.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc874b5c843e642f45fd85cda1ce599e123308ad2901ead23d3510a47ff506d1", size = 3102894, upload-time = "2025-06-20T21:48:28.114Z" }, + { url = "https://files.pythonhosted.org/packages/d0/54/0fcf2b619720a26fbb6cc941e89f2472a522cd963a776c089b189559447f/hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dbba1660e5d810bd0ea77c511a99e9242d920790d0e63c0e4673ed36c4022d18", size = 3002134, upload-time = "2025-06-20T21:48:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/1d351ac6cef7c4ba8c85744d37ffbfac2d53d0a6c04d2cabeba614640a78/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab34c4c3104133c495785d5d8bba3b1efc99de52c02e759cf711a91fd39d3a14", size = 3171009, upload-time = "2025-06-20T21:48:33.987Z" }, + { url = "https://files.pythonhosted.org/packages/c9/65/4b2ddb0e3e983f2508528eb4501288ae2f84963586fbdfae596836d5e57a/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:83088ecea236d5113de478acb2339f92c95b4fb0462acaa30621fac02f5a534a", size = 3279245, upload-time = "2025-06-20T21:48:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/f0/55/ef77a85ee443ae05a9e9cba1c9f0dd9241eb42da2aeba1dc50f51154c81a/hf_xet-1.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:73e167d9807d166596b4b2f0b585c6d5bd84a26dea32843665a8b58f6edba245", size = 2738931, upload-time = "2025-06-20T21:48:39.482Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.135.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/a5/d4f74ba61bbe5dd001c998ae8b85f9bfdc6cd29e6c5693d1116847b64251/hypothesis-6.135.14.tar.gz", hash = "sha256:2666df50b3cc40ea08b161a5389d6a1cd5aa3cab0dd8fde0ae339389714a4f67", size = 452884, upload-time = "2025-06-20T19:16:38.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/cf/491a487229b04a2ad56175c74700cfb79635dfce2d942becc6ab10c0ceb9/hypothesis-6.135.14-py3-none-any.whl", hash = "sha256:0dd5b8095e36bd288367c631f864a16c30500b01b17943dcea681233f7421860", size = 519115, upload-time = "2025-06-20T19:16:34.539Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "libraries" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "fastapi-pagination" }, + { name = "flet", extra = ["all"] }, + { name = "httpx" }, + { name = "hypothesis" }, + { name = "nicegui" }, + { name = "pydantic-ai" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "reflex" }, + { name = "rich" }, + { name = "tabulate" }, + { name = "textual" }, + { name = "toolz" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.13" }, + { name = "fastapi-pagination", specifier = ">=0.13.2" }, + { name = "flet", extras = ["all"], specifier = ">=0.28.3" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "hypothesis", specifier = ">=6.135.14" }, + { name = "nicegui", specifier = ">=2.20.0" }, + { name = "pydantic-ai", specifier = ">=0.3.2" }, + { name = "pydantic-settings", specifier = ">=2.10.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "reflex", specifier = ">=0.7.14" }, + { name = "rich", specifier = ">=14.0.0" }, + { name = "tabulate", specifier = ">=0.9.0" }, + { name = "textual", specifier = ">=3.5.0" }, + { name = "toolz", specifier = ">=1.0.0" }, + { name = "uvicorn", specifier = ">=0.34.3" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "logfire-api" +version = "3.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/3a/d44e4a8e7906821a444fdfd64428a858b26fe222d1c4ed74dcd4d25556f2/logfire_api-3.21.1.tar.gz", hash = "sha256:3af7818c1d831da027667d2eeff8f8993d793eb5063e03d817b8cda90ddca1a8", size = 49362, upload-time = "2025-06-18T12:57:42.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/fe/36c8b8b66834d568d28a87de1cab4cb163f1358ac58dd8a0145db12f04e2/logfire_api-3.21.1-py3-none-any.whl", hash = "sha256:c85888e8f4df806b389c9f851ee5db044e2451dd8813ba0dd6a6c2279a8b8edb", size = 82482, upload-time = "2025-06-18T12:57:39.473Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] +plugins = [ + { name = "mdit-py-plugins" }, +] + +[[package]] +name = "markdown2" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/52/d7dcc6284d59edb8301b8400435fbb4926a9b0f13a12b5cbaf3a4a54bb7b/markdown2-2.5.3.tar.gz", hash = "sha256:4d502953a4633408b0ab3ec503c5d6984d1b14307e32b325ec7d16ea57524895", size = 141676, upload-time = "2025-01-24T21:13:55.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/37/0a13c83ccf5365b8e08ea572dfbc04b8cb87cadd359b2451a567f5248878/markdown2-2.5.3-py3-none-any.whl", hash = "sha256:a8ebb7e84b8519c37bf7382b3db600f1798a22c245bfd754a1f87ca8d7ea63b3", size = 48550, upload-time = "2025-01-24T21:13:49.937Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mcp" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294, upload-time = "2025-06-12T08:20:30.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistralai" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/38/6bc1ee13d73f2ca80e8dd172aee408d5699fd89eb00b5a0f3b0a96632a40/mistralai-1.8.2.tar.gz", hash = "sha256:3a2fdf35498dd71cca3ee065adf8d75331f3bc6bbfbc7ffdd20dc82ae01d9d6d", size = 176102, upload-time = "2025-06-10T17:31:11.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/85/088e4ec778879d8d3d4aa83549444c39f639d060422a6ef725029e8cfc9d/mistralai-1.8.2-py3-none-any.whl", hash = "sha256:d7f2c3c9d02475c1f1911cff2458bd01e91bbe8e15bfb57cb7ac397a9440ef8e", size = 374066, upload-time = "2025-06-10T17:31:10.461Z" }, +] + +[[package]] +name = "multidict" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/c9/092c4e9402b6d16de761cff88cb842a5c8cc50ccecaf9c4481ba53264b9e/multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456", size = 73486, upload-time = "2025-06-17T14:14:37.238Z" }, + { url = "https://files.pythonhosted.org/packages/08/f9/6f7ddb8213f5fdf4db48d1d640b78e8aef89b63a5de8a2313286db709250/multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99", size = 43745, upload-time = "2025-06-17T14:14:38.32Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/b9be0163bfeee3bb08a77a1705e24eb7e651d594ea554107fac8a1ca6a4d/multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a", size = 42135, upload-time = "2025-06-17T14:14:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/93c8203f943a417bda3c573a34d5db0cf733afdfffb0ca78545c7716dbd8/multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb", size = 238585, upload-time = "2025-06-17T14:14:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fe/2582b56a1807604774f566eeef183b0d6b148f4b89d1612cd077567b2e1e/multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617", size = 236174, upload-time = "2025-06-17T14:14:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c4/d8b66d42d385bd4f974cbd1eaa8b265e6b8d297249009f312081d5ded5c7/multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855", size = 250145, upload-time = "2025-06-17T14:14:43.944Z" }, + { url = "https://files.pythonhosted.org/packages/bc/64/62feda5093ee852426aae3df86fab079f8bf1cdbe403e1078c94672ad3ec/multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be", size = 243470, upload-time = "2025-06-17T14:14:45.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/9f6fa6e854625cf289c0e9f4464b40212a01f76b2f3edfe89b6779b4fb93/multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75", size = 236968, upload-time = "2025-06-17T14:14:46.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/ae/4b81c6e3745faee81a156f3f87402315bdccf04236f75c03e37be19c94ff/multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826", size = 236575, upload-time = "2025-06-17T14:14:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/4089d7642ea344226e1bfab60dd588761d4791754f8072e911836a39bedf/multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a", size = 247632, upload-time = "2025-06-17T14:14:49.525Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/a353dac797de0f28fb7f078cc181c5f2eefe8dd16aa11a7100cbdc234037/multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73", size = 243520, upload-time = "2025-06-17T14:14:50.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/560deb3d2d95822d6eb1bcb1f1cb728f8f0197ec25be7c936d5d6a5d133c/multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7", size = 248551, upload-time = "2025-06-17T14:14:52.229Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/ddf277e67c78205f6695f2a7639be459bca9cc353b962fd8085a492a262f/multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10", size = 258362, upload-time = "2025-06-17T14:14:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/02/fc/d64ee1df9b87c5210f2d4c419cab07f28589c81b4e5711eda05a122d0614/multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a", size = 253862, upload-time = "2025-06-17T14:14:55.323Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/a2743c00d9e25f4826d3a77cc13d4746398872cf21c843eef96bb9945665/multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b", size = 247391, upload-time = "2025-06-17T14:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/7773518db74c442904dbd349074f1e7f2a854cee4d9529fc59e623d3949e/multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af", size = 41115, upload-time = "2025-06-17T14:14:59.33Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9a/6fc51b1dc11a7baa944bc101a92167d8b0f5929d376a8c65168fc0d35917/multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06", size = 44768, upload-time = "2025-06-17T14:15:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/82/2d/0d010be24b663b3c16e3d3307bbba2de5ae8eec496f6027d5c0515b371a8/multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2", size = 41770, upload-time = "2025-06-17T14:15:01.854Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/a71711a5f32f84b7b036e82182e3250b949a0ce70d51a2c6a4079e665449/multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a", size = 80450, upload-time = "2025-06-17T14:15:02.968Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a2/953a9eede63a98fcec2c1a2c1a0d88de120056219931013b871884f51b43/multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676", size = 46971, upload-time = "2025-06-17T14:15:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/44/61/60250212953459edda2c729e1d85130912f23c67bd4f585546fe4bdb1578/multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b", size = 45548, upload-time = "2025-06-17T14:15:05.666Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/e78ee82e96c495bc2582b303f68bed176b481c8d81a441fec07404fce2ca/multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d", size = 238545, upload-time = "2025-06-17T14:15:06.88Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0f/6132ca06670c8d7b374c3a4fd1ba896fc37fbb66b0de903f61db7d1020ec/multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d", size = 229931, upload-time = "2025-06-17T14:15:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/d9957c506e6df6b3e7a194f0eea62955c12875e454b978f18262a65d017b/multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14", size = 248181, upload-time = "2025-06-17T14:15:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/43/3f/7d5490579640db5999a948e2c41d4a0efd91a75989bda3e0a03a79c92be2/multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6", size = 241846, upload-time = "2025-06-17T14:15:11.596Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/252b1ce949ece52bba4c0de7aa2e3a3d5964e800bce71fb778c2e6c66f7c/multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887", size = 232893, upload-time = "2025-06-17T14:15:12.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/0070bfd48c16afc26e056f2acce49e853c0d604a69c7124bc0bbdb1bcc0a/multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921", size = 228567, upload-time = "2025-06-17T14:15:14.267Z" }, + { url = "https://files.pythonhosted.org/packages/2a/31/90551c75322113ebf5fd9c5422e8641d6952f6edaf6b6c07fdc49b1bebdd/multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684", size = 246188, upload-time = "2025-06-17T14:15:15.985Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e2/aa4b02a55e7767ff292871023817fe4db83668d514dab7ccbce25eaf7659/multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6", size = 235178, upload-time = "2025-06-17T14:15:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5c/f67e726717c4b138b166be1700e2b56e06fbbcb84643d15f9a9d7335ff41/multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3", size = 243422, upload-time = "2025-06-17T14:15:18.939Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/15fa318285e26a50aa3fa979bbcffb90f9b4d5ec58882d0590eda067d0da/multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34", size = 254898, upload-time = "2025-06-17T14:15:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3d/d6c6d1c2e9b61ca80313912d30bb90d4179335405e421ef0a164eac2c0f9/multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068", size = 247129, upload-time = "2025-06-17T14:15:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" }, + { url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" }, + { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, +] + +[[package]] +name = "nicegui" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "certifi" }, + { name = "docutils" }, + { name = "fastapi" }, + { name = "h11" }, + { name = "httpx" }, + { name = "ifaddr" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markdown2" }, + { name = "orjson", marker = "platform_machine != 'i386' and platform_machine != 'i686'" }, + { name = "pygments" }, + { name = "python-engineio" }, + { name = "python-multipart" }, + { name = "python-socketio", extra = ["asyncio-client"] }, + { name = "requests" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "vbuild" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/ec/e2bf3f4858def66e990aa616d88a63567198b13532f0771d2b7ed5117ddd/nicegui-2.20.0.tar.gz", hash = "sha256:b238df9c3e9f915d6f74b005e5a66f40f0a7a0598bf9f8fcc5b03326b1b704f7", size = 13097951, upload-time = "2025-06-13T14:05:18.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/df/24977fe7ee9c2205e99e6d4a74220d278b8169bc8cfe441ad90fb88773aa/nicegui-2.20.0-py3-none-any.whl", hash = "sha256:db550537e2664e916080bc381bf29d0659570e42cdceb224b94a418e4db85c8c", size = 13479430, upload-time = "2025-06-13T14:05:15.173Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "openai" +version = "1.91.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/e2/a22f2973b729eff3f1f429017bdf717930c5de0fbf9e14017bae330e4e7a/openai-1.91.0.tar.gz", hash = "sha256:d6b07730d2f7c6745d0991997c16f85cddfc90ddcde8d569c862c30716b9fc90", size = 472529, upload-time = "2025-06-23T18:27:10.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d2/f99bdd6fc737d6b3cf0df895508d621fc9a386b375a1230ee81d46c5436e/openai-1.91.0-py3-none-any.whl", hash = "sha256:207f87aa3bc49365e014fac2f7e291b99929f4fe126c4654143440e0ad446a5f", size = 735837, upload-time = "2025-06-23T18:27:08.913Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087, upload-time = "2025-04-29T23:29:19.083Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273, upload-time = "2025-04-29T23:29:20.602Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779, upload-time = "2025-04-29T23:29:22.062Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811, upload-time = "2025-04-29T23:29:23.602Z" }, + { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018, upload-time = "2025-04-29T23:29:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368, upload-time = "2025-04-29T23:29:26.609Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840, upload-time = "2025-04-29T23:29:28.153Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135, upload-time = "2025-04-29T23:29:29.726Z" }, + { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810, upload-time = "2025-04-29T23:29:31.269Z" }, + { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491, upload-time = "2025-04-29T23:29:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277, upload-time = "2025-04-29T23:29:34.946Z" }, + { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367, upload-time = "2025-04-29T23:29:36.52Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "pscript" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/68/f918702e270eddc5f7c54108f6a2f2afc2d299985820dbb0db9beb77d66d/pscript-0.7.7.tar.gz", hash = "sha256:8632f7a4483f235514aadee110edee82eb6d67336bf68744a7b18d76e50442f8", size = 176138, upload-time = "2022-01-10T10:55:02.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/bc/980e2ebd442d2a8f1d22780f73db76f2a1df3bf79b3fb501b054b4b4dd03/pscript-0.7.7-py3-none-any.whl", hash = "sha256:b0fdac0df0393a4d7497153fea6a82e6429f32327c4c0a4817f1cd68adc08083", size = 126689, upload-time = "2022-01-10T10:55:00.793Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-ai" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["a2a", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "mcp", "mistral", "openai", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4b/6e2025e48e19be64439fca67a915b225fad0d8dd5938834cff2277972d76/pydantic_ai-0.3.2.tar.gz", hash = "sha256:7ce4afcc025afbc166631ccb2b221bc633249fea0e048091ef41db28243f3467", size = 40676919, upload-time = "2025-06-21T05:25:09.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/5a/2f111433977b2a8b6c157aae0ed797618e3a8eafdf5be5813311ef7cb816/pydantic_ai-0.3.2-py3-none-any.whl", hash = "sha256:7d7b0695e5ba185bc4b6252f9eef724ddb89172565323b758f2a8faaa64ef513", size = 10124, upload-time = "2025-06-21T05:24:59.872Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "griffe" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/96/aa71914c14cb09801e6637b63e3bfaefb1b10e512a9f49d0cd1dd6f67a21/pydantic_ai_slim-0.3.2.tar.gz", hash = "sha256:90f1e6d95d0bbffbca118619b3b3e0f16c5c2c281e4c8c2ec66467b8e8615621", size = 151673, upload-time = "2025-06-21T05:25:13.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/df/d9adb57ffc13e25c40c1b450814950d315dfb3b6c3af150373a4c14a12be/pydantic_ai_slim-0.3.2-py3-none-any.whl", hash = "sha256:c409f00de1921cb610cab46f07a7b55b0632be7b8b87e3609573b47c07cb5ef1", size = 202200, upload-time = "2025-06-21T05:25:03.306Z" }, +] + +[package.optional-dependencies] +a2a = [ + { name = "fasta2a" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai" }, +] +groq = [ + { name = "groq" }, +] +mcp = [ + { name = "mcp" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/a9/3ea4eb5572f690bc422cc96a25b84729c86ed38bfa59317bf801c089f441/pydantic_evals-0.3.2.tar.gz", hash = "sha256:9034e2b51425ea125ebff347542362d70d92c8be73a4af58282fc5b58f09f6b0", size = 42914, upload-time = "2025-06-21T05:25:15.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/38/18b16b55b16c25986bee6f86a635fb7260f5c490ddfdd8888838b227cf92/pydantic_evals-0.3.2-py3-none-any.whl", hash = "sha256:d7c5b133ce8cb3dd56c748d62b1618ba743b91459c2bf64e835d650cd0752a0b", size = 51633, upload-time = "2025-06-21T05:25:04.632Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/e5/b114a97f3cbbbe15d193329a83d5297cf911f1c62f38398bc31b7218a806/pydantic_graph-0.3.2.tar.gz", hash = "sha256:874b06d6484499e391a2f799bb3b5399420e5d786087012a8716a398bfc3aeec", size = 21858, upload-time = "2025-06-21T05:25:16.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2c/8c2396eafac80da93c84e724ca277d2f8bb6b8c32f57ad2b1caa85546eba/pydantic_graph-0.3.2-py3-none-any.whl", hash = "sha256:efab29d7f201ad7a199acd94bb4d8accd70cc756e4030c069ac0d1048cb543a2", size = 27483, upload-time = "2025-06-21T05:25:05.765Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/3d61472b7801c896f9efd9bb8750977d9577098b05224c5c41820690155e/pydantic_settings-2.10.0.tar.gz", hash = "sha256:7a12e0767ba283954f3fd3fefdd0df3af21b28aa849c40c35811d52d682fa876", size = 172625, upload-time = "2025-06-21T13:56:55.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/9e/fce9331fecf1d2761ff0516c5dceab8a5fd415e82943e727dc4c5fa84a90/pydantic_settings-2.10.0-py3-none-any.whl", hash = "sha256:33781dfa1c7405d5ed2b6f150830a93bb58462a847357bd8f162f8bacb77c027", size = 45232, upload-time = "2025-06-21T13:56:53.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pypng" +version = "0.20220715.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/cd/112f092ec27cca83e0516de0a3368dbd9128c187fb6b52aaaa7cde39c96d/pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1", size = 128992, upload-time = "2022-07-15T14:11:05.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/b9/3766cc361d93edb2ce81e2e1f87dd98f314d7d513877a342d31b30741680/pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", size = 58057, upload-time = "2022-07-15T14:11:03.713Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-engineio" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0b/67295279b66835f9fa7a491650efcd78b20321c127036eef62c11a31e028/python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa", size = 91677, upload-time = "2025-06-04T19:22:18.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/fa/df59acedf7bbb937f69174d00f921a7b93aa5a5f5c17d05296c814fff6fc/python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f", size = 59536, upload-time = "2025-06-04T19:22:16.916Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "python-socketio" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/1a/396d50ccf06ee539fa758ce5623b59a9cb27637fc4b2dc07ed08bf495e77/python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029", size = 121125, upload-time = "2025-04-12T15:46:59.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800, upload-time = "2025-04-12T15:46:58.412Z" }, +] + +[package.optional-dependencies] +asyncio-client = [ + { name = "aiohttp" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "qrcode" +version = "7.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pypng" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/35/ad6d4c5a547fe9a5baf85a9edbafff93fc6394b014fab30595877305fa59/qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845", size = 535974, upload-time = "2023-02-05T22:11:46.548Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/79/aaf0c1c7214f2632badb2771d770b1500d3d7cbdf2590ae62e721ec50584/qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", size = 46197, upload-time = "2023-02-05T22:11:43.4Z" }, +] + +[[package]] +name = "redis" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" }, +] + +[[package]] +name = "reflex" +version = "0.7.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "click" }, + { name = "fastapi" }, + { name = "granian", extra = ["reload"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "python-multipart" }, + { name = "python-socketio" }, + { name = "redis" }, + { name = "reflex-hosting-cli" }, + { name = "rich" }, + { name = "sqlmodel" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/11/52994ec428691991959d486fa48df57fe6a571a5d8d98ebd590377bf7f07/reflex-0.7.14.tar.gz", hash = "sha256:9a36eee47e60c64630cdc341565416ee838a53ee3c60d281eea0b7887b67f1e3", size = 576039, upload-time = "2025-06-03T02:01:54.855Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e2/9dc1156cbfc1b7cde889ab09f89b0ae0938e694c32c4c553c52f3ad0796b/reflex-0.7.14-py3-none-any.whl", hash = "sha256:7b1e0e37a88e9129aa896c5a47c212a29c620fa2e82148813cd335c172974948", size = 873575, upload-time = "2025-06-03T02:01:52.853Z" }, +] + +[[package]] +name = "reflex-hosting-cli" +version = "0.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/81/305748b296140110d5cdd6441eaddc6058dc8906d4a8b13f9fa97d02bac0/reflex_hosting_cli-0.1.50.tar.gz", hash = "sha256:d594d3734f4fff4ad33618aab0d0c1d9130b7148cab75ac85adb9f6a49055a48", size = 32280, upload-time = "2025-06-07T22:46:46.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/b9/e1802ceffc9dc026cad2c8de180789b72f5f3aa7be7d4da33645cbc2a75e/reflex_hosting_cli-0.1.50-py3-none-any.whl", hash = "sha256:5285f9eaf80858e6c5dca6cd705c0f5924a243697f58814938a73f1083cab143", size = 41226, upload-time = "2025-06-07T22:46:44.951Z" }, +] + +[[package]] +name = "repath" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/e1/824989291d0f01886074fdf9504ba54598f5665bc4dd373b589b87e76608/repath-0.9.0.tar.gz", hash = "sha256:8292139bac6a0e43fd9d70605d4e8daeb25d46672e484ed31a24c7ce0aef0fb7", size = 5492, upload-time = "2019-10-08T00:25:22.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ed/92e9b8a3ffc562f21df14ef2538f54e911df29730e1f0d79130a4edc86e7/repath-0.9.0-py3-none-any.whl", hash = "sha256:ee079d6c91faeb843274d22d8f786094ee01316ecfe293a1eb6546312bb6a318", size = 4738, upload-time = "2019-10-08T00:25:20.842Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" }, +] + +[[package]] +name = "sse-starlette" +version = "2.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "textual" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/63/16cdf4b9efb47366940d8315118c5c6dd6309f5eb2c159d7195b60e2e221/textual-3.5.0.tar.gz", hash = "sha256:c4a440338694672acf271c74904f1cf1e4a64c6761c056b02a561774b81a04f4", size = 1590084, upload-time = "2025-06-20T14:46:58.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/36/2597036cb80e40f71555bf59741471f7bd76ebed112f10ae0549650a12bf/textual-3.5.0-py3-none-any.whl", hash = "sha256:7c960efb70391b754e66201776793de2b26d699d51fb91f5f78401d13cec79a1", size = 688740, upload-time = "2025-06-20T14:46:56.484Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790, upload-time = "2024-10-04T16:17:04.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "vbuild" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pscript" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/be/f0c6204a36440bbcc086bfa25964d009b7391c5a3c74d6e73188efd47adb/vbuild-0.8.2.tar.gz", hash = "sha256:270cd9078349d907dfae6c0e6364a5a5e74cb86183bb5093613f12a18b435fa9", size = 8937, upload-time = "2023-08-03T09:26:36.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/3d/7b22abbdb059d551507275a2815bc2b1974e3b9f6a13781c1eac9e858965/vbuild-0.8.2-py2.py3-none-any.whl", hash = "sha256:d76bcc976a1c53b6a5776ac947606f9e7786c25df33a587ebe33ed09dd8a1076", size = 9371, upload-time = "2023-08-03T09:26:35.023Z" }, +] + +[[package]] +name = "watchdog" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587, upload-time = "2024-08-11T07:38:01.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343, upload-time = "2024-08-11T07:37:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313, upload-time = "2024-08-11T07:37:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919, upload-time = "2024-08-11T07:37:24.715Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947, upload-time = "2024-08-11T07:37:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942, upload-time = "2024-08-11T07:37:46.722Z" }, + { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947, upload-time = "2024-08-11T07:37:48.941Z" }, + { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946, upload-time = "2024-08-11T07:37:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947, upload-time = "2024-08-11T07:37:51.55Z" }, + { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944, upload-time = "2024-08-11T07:37:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947, upload-time = "2024-08-11T07:37:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935, upload-time = "2024-08-11T07:37:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934, upload-time = "2024-08-11T07:37:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933, upload-time = "2024-08-11T07:37:59.573Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, + { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" }, + { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" }, + { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" }, + { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" }, + { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" }, + { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" }, + { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 6a4e0d85babd500ec128fd6bc4806f7387115dd4 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 17 Jun 2025 13:31:25 +0200 Subject: [PATCH 025/113] Builder pattern example. --- 2025/builder/htmlbuilder.py | 45 +++++++++++++++++++++++++++++++++++++ 2025/builder/main.py | 28 +++++++++++++++++++++++ 2025/builder/pyproject.toml | 6 +++++ 2025/builder/uv.lock | 8 +++++++ 2025/builder/viewer.py | 32 ++++++++++++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 2025/builder/htmlbuilder.py create mode 100644 2025/builder/main.py create mode 100644 2025/builder/pyproject.toml create mode 100644 2025/builder/uv.lock create mode 100644 2025/builder/viewer.py diff --git a/2025/builder/htmlbuilder.py b/2025/builder/htmlbuilder.py new file mode 100644 index 00000000..2e20f00f --- /dev/null +++ b/2025/builder/htmlbuilder.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import Self + + +@dataclass +class HTMLPage: + title: str + body_elements: list[str] + + def render(self) -> str: + body = "\n".join(self.body_elements) + return f""" + + {self.title} + + {body} + + """ + + +class HTMLBuilder: + def __init__(self) -> None: + self._title: str = "Untitled" + self._body: list[str] = [] + + def set_title(self, title: str) -> Self: + self._title = title + return self + + def add_header(self, text: str, level: int = 1) -> Self: + self._body.append(f"{text}") + return self + + def add_paragraph(self, text: str) -> Self: + self._body.append(f"

{text}

") + return self + + def add_button(self, label: str, onclick: str = "#") -> Self: + self._body.append( + f"" + ) + return self + + def build(self) -> HTMLPage: + return HTMLPage(self._title, self._body) diff --git a/2025/builder/main.py b/2025/builder/main.py new file mode 100644 index 00000000..89e85b95 --- /dev/null +++ b/2025/builder/main.py @@ -0,0 +1,28 @@ +from htmlbuilder import HTMLBuilder +from viewer import HTMLViewer + + +def main() -> None: + # --- Build UI Page --- + builder = HTMLBuilder() + page = ( + builder.set_title("Builder Pattern UI") + .add_header("Hello from Python!", level=1) + .add_paragraph("This page was generated using the Builder Pattern.") + .add_button("Visit ArjanCodes", onclick="https://arjan.codes") + .build() + ) + + file_path = "page.html" + with open(file_path, "w") as f: + f.write(page.render()) + + print("HTML page written to 'page.html'") + + # --- Start Viewer --- + viewer = HTMLViewer(filename=file_path) + viewer.start() + + +if __name__ == "__main__": + main() diff --git a/2025/builder/pyproject.toml b/2025/builder/pyproject.toml new file mode 100644 index 00000000..d0f71cd1 --- /dev/null +++ b/2025/builder/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "builder" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ +] diff --git a/2025/builder/uv.lock b/2025/builder/uv.lock new file mode 100644 index 00000000..e7762de6 --- /dev/null +++ b/2025/builder/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "builder" +version = "0.1.0" +source = { virtual = "." } diff --git a/2025/builder/viewer.py b/2025/builder/viewer.py new file mode 100644 index 00000000..45ba5c9f --- /dev/null +++ b/2025/builder/viewer.py @@ -0,0 +1,32 @@ +import http.server +import os +import socketserver +import webbrowser + + +class HTMLViewer: + def __init__(self, filename: str, port: int = 8000) -> None: + self.filename = filename + self.port = port + self._server: socketserver.TCPServer | None = None + + def _open_browser(self) -> None: + url = f"http://localhost:{self.port}/{self.filename}" + webbrowser.open(url) + + def _serve(self) -> None: + handler = http.server.SimpleHTTPRequestHandler + with socketserver.TCPServer(("", self.port), handler) as httpd: + self._server = httpd + self._open_browser() + print(f"Serving '{self.filename}' at http://localhost:{self.port}") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received. Shutting down server...") + httpd.shutdown() + httpd.server_close() + + def start(self) -> None: + os.chdir(os.path.dirname(os.path.abspath(self.filename))) + self._serve() From 64225b395988651aa16db16a8d5e56432a257f52 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Jun 2025 09:54:57 +0200 Subject: [PATCH 026/113] Updated website. Made dataclass immutable. Added metadata to builder and page. --- 2025/builder/htmlbuilder.py | 19 ++++++++++++++++--- 2025/builder/main.py | 3 ++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/2025/builder/htmlbuilder.py b/2025/builder/htmlbuilder.py index 2e20f00f..ad3d64f8 100644 --- a/2025/builder/htmlbuilder.py +++ b/2025/builder/htmlbuilder.py @@ -2,16 +2,24 @@ from typing import Self -@dataclass +@dataclass(frozen=True) class HTMLPage: title: str + metadata: dict[str, str] body_elements: list[str] def render(self) -> str: body = "\n".join(self.body_elements) + meta_tags = "\n".join( + f'' + for name, value in self.metadata.items() + ) return f""" - {self.title} + + {self.title} + {meta_tags} + {body} @@ -22,6 +30,7 @@ class HTMLBuilder: def __init__(self) -> None: self._title: str = "Untitled" self._body: list[str] = [] + self._metadata: dict[str, str] = {} def set_title(self, title: str) -> Self: self._title = title @@ -41,5 +50,9 @@ def add_button(self, label: str, onclick: str = "#") -> Self: ) return self + def add_metadata(self, name: str, content: str) -> Self: + self._metadata[name] = content + return self + def build(self) -> HTMLPage: - return HTMLPage(self._title, self._body) + return HTMLPage(self._title, self._metadata, self._body) diff --git a/2025/builder/main.py b/2025/builder/main.py index 89e85b95..e3c88b9a 100644 --- a/2025/builder/main.py +++ b/2025/builder/main.py @@ -9,10 +9,11 @@ def main() -> None: builder.set_title("Builder Pattern UI") .add_header("Hello from Python!", level=1) .add_paragraph("This page was generated using the Builder Pattern.") - .add_button("Visit ArjanCodes", onclick="https://arjan.codes") + .add_button("Visit ArjanCodes", onclick="https://www.arjancodes.com") .build() ) + # --- Write to HTML File --- file_path = "page.html" with open(file_path, "w") as f: f.write(page.render()) From f9f4831a024c936e7371951a44eacac98aeaa98b Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Jun 2025 12:28:40 +0200 Subject: [PATCH 027/113] Added other library examples. --- 2025/builder/matplotlib_example.py | 16 ++ 2025/builder/pandas_example.py | 11 + 2025/builder/pyproject.toml | 3 + 2025/builder/uv.lock | 320 +++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 2025/builder/matplotlib_example.py create mode 100644 2025/builder/pandas_example.py diff --git a/2025/builder/matplotlib_example.py b/2025/builder/matplotlib_example.py new file mode 100644 index 00000000..79200794 --- /dev/null +++ b/2025/builder/matplotlib_example.py @@ -0,0 +1,16 @@ +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() + +fruits = ["apple", "blueberry", "cherry", "orange"] +counts = [40, 100, 30, 55] +bar_labels = ["red", "blue", "_red", "orange"] +bar_colors = ["tab:red", "tab:blue", "tab:red", "tab:orange"] + +ax.bar(fruits, counts, label=bar_labels, color=bar_colors) + +ax.set_ylabel("fruit supply") +ax.set_title("Fruit supply by kind and color") +ax.legend(title="Fruit color") + +plt.show() diff --git a/2025/builder/pandas_example.py b/2025/builder/pandas_example.py new file mode 100644 index 00000000..cdea2509 --- /dev/null +++ b/2025/builder/pandas_example.py @@ -0,0 +1,11 @@ +import pandas as pd + +df = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) +styled = ( + df.style.set_caption("Styled DataFrame") + .highlight_max(axis=0) + .format("{:.2f}") + .background_gradient(cmap="viridis") +) + +print(styled.to_html()) diff --git a/2025/builder/pyproject.toml b/2025/builder/pyproject.toml index d0f71cd1..cf6e4746 100644 --- a/2025/builder/pyproject.toml +++ b/2025/builder/pyproject.toml @@ -3,4 +3,7 @@ name = "builder" version = "0.1.0" requires-python = ">=3.13" dependencies = [ + "jinja2>=3.1.6", + "matplotlib>=3.10.3", + "pandas>=2.3.0", ] diff --git a/2025/builder/uv.lock b/2025/builder/uv.lock index e7762de6..d2431f43 100644 --- a/2025/builder/uv.lock +++ b/2025/builder/uv.lock @@ -6,3 +6,323 @@ requires-python = ">=3.13" name = "builder" version = "0.1.0" source = { virtual = "." } +dependencies = [ + { name = "jinja2" }, + { name = "matplotlib" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "matplotlib", specifier = ">=3.10.3" }, + { name = "pandas", specifier = ">=2.3.0" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "fonttools" +version = "4.58.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/4f/c05cab5fc1a4293e6bc535c6cb272607155a0517700f5418a4165b7f9ec8/fonttools-4.58.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5f4a64846495c543796fa59b90b7a7a9dff6839bd852741ab35a71994d685c6d", size = 2745197, upload-time = "2025-06-13T17:24:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d3/49211b1f96ae49308f4f78ca7664742377a6867f00f704cdb31b57e4b432/fonttools-4.58.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e80661793a5d4d7ad132a2aa1eae2e160fbdbb50831a0edf37c7c63b2ed36574", size = 2317272, upload-time = "2025-06-13T17:24:43.428Z" }, + { url = "https://files.pythonhosted.org/packages/b2/11/c9972e46a6abd752a40a46960e431c795ad1f306775fc1f9e8c3081a1274/fonttools-4.58.4-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe5807fc64e4ba5130f1974c045a6e8d795f3b7fb6debfa511d1773290dbb76b", size = 4877184, upload-time = "2025-06-13T17:24:45.527Z" }, + { url = "https://files.pythonhosted.org/packages/ea/24/5017c01c9ef8df572cc9eaf9f12be83ad8ed722ff6dc67991d3d752956e4/fonttools-4.58.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b610b9bef841cb8f4b50472494158b1e347d15cad56eac414c722eda695a6cfd", size = 4939445, upload-time = "2025-06-13T17:24:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/538cc4d0284b5a8826b4abed93a69db52e358525d4b55c47c8cef3669767/fonttools-4.58.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2daa7f0e213c38f05f054eb5e1730bd0424aebddbeac094489ea1585807dd187", size = 4878800, upload-time = "2025-06-13T17:24:49.766Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9b/a891446b7a8250e65bffceb248508587958a94db467ffd33972723ab86c9/fonttools-4.58.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66cccb6c0b944496b7f26450e9a66e997739c513ffaac728d24930df2fd9d35b", size = 5021259, upload-time = "2025-06-13T17:24:51.754Z" }, + { url = "https://files.pythonhosted.org/packages/17/b2/c4d2872cff3ace3ddd1388bf15b76a1d8d5313f0a61f234e9aed287e674d/fonttools-4.58.4-cp313-cp313-win32.whl", hash = "sha256:94d2aebb5ca59a5107825520fde596e344652c1f18170ef01dacbe48fa60c889", size = 2185824, upload-time = "2025-06-13T17:24:54.324Z" }, + { url = "https://files.pythonhosted.org/packages/98/57/cddf8bcc911d4f47dfca1956c1e3aeeb9f7c9b8e88b2a312fe8c22714e0b/fonttools-4.58.4-cp313-cp313-win_amd64.whl", hash = "sha256:b554bd6e80bba582fd326ddab296e563c20c64dca816d5e30489760e0c41529f", size = 2236382, upload-time = "2025-06-13T17:24:56.291Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" }, + { url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" }, + { url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" }, + { url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, + { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, + { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] From ffb8127be1cd1c5858b632f722d4c599ec9b5f41 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 25 Jun 2025 16:56:16 +0200 Subject: [PATCH 028/113] Fixed type issue in base API model. --- 2025/sdk/sdk_v3/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2025/sdk/sdk_v3/base.py b/2025/sdk/sdk_v3/base.py index 38313ff8..64bda527 100644 --- a/2025/sdk/sdk_v3/base.py +++ b/2025/sdk/sdk_v3/base.py @@ -5,7 +5,7 @@ from sdk_v3 import client -class BaseAPIModel[T](BaseModel): +class BaseAPIModel[T: BaseAPIModel](BaseModel): id: str | None = None _resource_path: ClassVar[str] = "" From 63b2dcd10796426c1f0c7cd422090e8363c8bf1a Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 15 Jul 2025 15:34:12 +0200 Subject: [PATCH 029/113] Added standard library code examples. --- 2025/standard/01_dataclasses.py | 17 +++++++++++++++++ 2025/standard/02_pathlib.py | 12 ++++++++++++ 2025/standard/03_functools.py | 23 +++++++++++++++++++++++ 2025/standard/04_tomllib.py | 6 ++++++ 2025/standard/05_graphlib.py | 17 +++++++++++++++++ 2025/standard/06_heapq.py | 28 ++++++++++++++++++++++++++++ 2025/standard/07_secrets.py | 22 ++++++++++++++++++++++ 2025/standard/08_shutil.py | 13 +++++++++++++ 2025/standard/09_textwrap.py | 22 ++++++++++++++++++++++ 2025/standard/10_itertools.py | 32 ++++++++++++++++++++++++++++++++ 2025/standard/pyproject.toml | 7 +++++++ 2025/standard/uv.lock | 8 ++++++++ 12 files changed, 207 insertions(+) create mode 100644 2025/standard/01_dataclasses.py create mode 100644 2025/standard/02_pathlib.py create mode 100644 2025/standard/03_functools.py create mode 100644 2025/standard/04_tomllib.py create mode 100644 2025/standard/05_graphlib.py create mode 100644 2025/standard/06_heapq.py create mode 100644 2025/standard/07_secrets.py create mode 100644 2025/standard/08_shutil.py create mode 100644 2025/standard/09_textwrap.py create mode 100644 2025/standard/10_itertools.py create mode 100644 2025/standard/pyproject.toml create mode 100644 2025/standard/uv.lock diff --git a/2025/standard/01_dataclasses.py b/2025/standard/01_dataclasses.py new file mode 100644 index 00000000..33853875 --- /dev/null +++ b/2025/standard/01_dataclasses.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass +class Product: + name: str + price: float + in_stock: bool = True + + +def main(): + product = Product(name="Widget", price=19.99) + print(product) + + +if __name__ == "__main__": + main() diff --git a/2025/standard/02_pathlib.py b/2025/standard/02_pathlib.py new file mode 100644 index 00000000..a50b232d --- /dev/null +++ b/2025/standard/02_pathlib.py @@ -0,0 +1,12 @@ +from pathlib import Path + +base = Path("my_project") +config = base / "config" / "settings.toml" + +print("Config path:", config) + +if config.exists(): + print("File size:", config.stat().st_size) +else: + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text("[settings]\nname = 'Example'") \ No newline at end of file diff --git a/2025/standard/03_functools.py b/2025/standard/03_functools.py new file mode 100644 index 00000000..e0942752 --- /dev/null +++ b/2025/standard/03_functools.py @@ -0,0 +1,23 @@ +from functools import cache, partial + +@cache +def power(base: int, exponent: int) -> int: + print(f"Computing {base}^{exponent}") + return base ** exponent + +def main() -> None: + # cached power function + print("Calculating powers with caching:") + print("Power of 2^10:", power(2, 10)) + print("Power of 3^5:", power(3, 5)) + print("Power of 2^10 again (cached):", power(2, 10)) + + square = partial(power, exponent=2) + cube = partial(power, exponent=3) + + print("Square of 5:", square(5)) + print("Cube of 2:", cube(2)) + print("Square of 5 again (cached):", square(5)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/04_tomllib.py b/2025/standard/04_tomllib.py new file mode 100644 index 00000000..921c68d5 --- /dev/null +++ b/2025/standard/04_tomllib.py @@ -0,0 +1,6 @@ +import tomllib + +with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + +print(data["project"]["name"]) \ No newline at end of file diff --git a/2025/standard/05_graphlib.py b/2025/standard/05_graphlib.py new file mode 100644 index 00000000..b8a277ad --- /dev/null +++ b/2025/standard/05_graphlib.py @@ -0,0 +1,17 @@ +from graphlib import TopologicalSorter + +ts = TopologicalSorter[str]() + +def main(): + # Tasks and their dependencies + ts.add("compile", "fetch_sources") + ts.add("test", "compile") + ts.add("package", "test") + ts.add("deploy", "package") + ts.add("fetch_sources") + + order = list(ts.static_order()) + print("Execution order:", order) + +if __name__ == "__main__": + main() diff --git a/2025/standard/06_heapq.py b/2025/standard/06_heapq.py new file mode 100644 index 00000000..9e55d01a --- /dev/null +++ b/2025/standard/06_heapq.py @@ -0,0 +1,28 @@ +import heapq + +def main() -> None: + # Initial tasks with priorities + tasks = [ + (3, "Send email to client"), + (2, "Write documentation"), + (1, "Fix critical bug"), + ] + + heapq.heapify(tasks) + + print("Starting task processing...") + + while tasks: + priority, task = heapq.heappop(tasks) + print(f"Processing task: {task} (priority {priority})") + + # Dynamically add new tasks + if task == "Fix critical bug": + print("New urgent task arrived!") + heapq.heappush(tasks, (0, "Deploy hotfix")) + + if task == "Write documentation": + heapq.heappush(tasks, (4, "Refactor old module")) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/07_secrets.py b/2025/standard/07_secrets.py new file mode 100644 index 00000000..33b29354 --- /dev/null +++ b/2025/standard/07_secrets.py @@ -0,0 +1,22 @@ +import secrets + +def main() -> None: + # Generate a secure hexadecimal token + token_hex: str = secrets.token_hex(16) + print("Secure hex token:", token_hex) + + # Generate a secure URL-safe token + token_url: str = secrets.token_urlsafe(16) + print("Secure URL-safe token:", token_url) + + # Generate random bytes + random_bytes: bytes = secrets.token_bytes(16) + print("Random bytes:", random_bytes) + + # Randomly choose an item from a sequence + choices = ["apple", "banana", "cherry"] + selected: str = secrets.choice(choices) + print("Random choice:", selected) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/08_shutil.py b/2025/standard/08_shutil.py new file mode 100644 index 00000000..0140e7c0 --- /dev/null +++ b/2025/standard/08_shutil.py @@ -0,0 +1,13 @@ +from py_compile import main +import shutil + +def main() -> None: + + # Copy a file + shutil.copy("pyproject.toml", "backup_pyproject.toml") + + # Create a zip archive + shutil.make_archive("project_backup", "zip", ".") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/09_textwrap.py b/2025/standard/09_textwrap.py new file mode 100644 index 00000000..0447d0bc --- /dev/null +++ b/2025/standard/09_textwrap.py @@ -0,0 +1,22 @@ +import textwrap + +def main() -> None: + text = ( + "Python is amazing. It has a huge standard library that saves you time " + "and helps you write clean, maintainable code." + ) + + wrapped = textwrap.fill(text, width=40) + print("Wrapped text:") + print(wrapped) + + indented = textwrap.indent(wrapped, prefix="> ") + print("\nIndented text:") + print(indented) + + shortened = textwrap.shorten(text, width=50, placeholder="...") + print("\nShortened text:") + print(shortened) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/10_itertools.py b/2025/standard/10_itertools.py new file mode 100644 index 00000000..92375ff2 --- /dev/null +++ b/2025/standard/10_itertools.py @@ -0,0 +1,32 @@ +import itertools +from collections.abc import Iterable + +def main() -> None: + items = ["a", "b", "c"] + + # All pairs of items + pairs = list(itertools.combinations(items, 2)) + print("Pairs:", pairs) + + # Infinite counter + counter = itertools.count(start=10, step=5) + print("First three counter values:") + print(next(counter)) + print(next(counter)) + print(next(counter)) + + # Cycle through items + cycle = itertools.cycle(["on", "off"]) + print("Cycle example:") + for _ in range(4): + print(next(cycle)) + + # Group consecutive identical items + data = ["a", "a", "b", "b", "b", "c", "a", "a"] + print("Grouping consecutive items:") + for key, group in itertools.groupby(data): + group_list = list(group) + print(f"{key}: {group_list}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/standard/pyproject.toml b/2025/standard/pyproject.toml new file mode 100644 index 00000000..6463b732 --- /dev/null +++ b/2025/standard/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "standard" +version = "0.1.0" +description = "Nice Python standard library tips" +requires-python = ">=3.13" +dependencies = [ +] diff --git a/2025/standard/uv.lock b/2025/standard/uv.lock new file mode 100644 index 00000000..e73db73c --- /dev/null +++ b/2025/standard/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "standard" +version = "0.1.0" +source = { virtual = "." } From b89fd1e27457514cf2ec7bd3e3a4acf951197f94 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 17 Jul 2025 16:08:10 +0200 Subject: [PATCH 030/113] Minor cleanup of code examples. --- 2025/standard/01_dataclasses.py | 2 +- 2025/standard/02_pathlib.py | 22 ++++++++++++++-------- 2025/standard/03_functools.py | 7 +++++-- 2025/standard/04_tomllib.py | 13 ++++++++++--- 2025/standard/05_graphlib.py | 4 +++- 2025/standard/08_shutil.py | 6 +++--- 2025/standard/10_itertools.py | 5 +++-- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/2025/standard/01_dataclasses.py b/2025/standard/01_dataclasses.py index 33853875..4ca9058d 100644 --- a/2025/standard/01_dataclasses.py +++ b/2025/standard/01_dataclasses.py @@ -8,7 +8,7 @@ class Product: in_stock: bool = True -def main(): +def main() -> None: product = Product(name="Widget", price=19.99) print(product) diff --git a/2025/standard/02_pathlib.py b/2025/standard/02_pathlib.py index a50b232d..ace4ee17 100644 --- a/2025/standard/02_pathlib.py +++ b/2025/standard/02_pathlib.py @@ -1,12 +1,18 @@ from pathlib import Path -base = Path("my_project") -config = base / "config" / "settings.toml" -print("Config path:", config) +def main() -> None: + base = Path("my_project") + config = base / "config" / "settings.toml" -if config.exists(): - print("File size:", config.stat().st_size) -else: - config.parent.mkdir(parents=True, exist_ok=True) - config.write_text("[settings]\nname = 'Example'") \ No newline at end of file + print("Config path:", config) + + if config.exists(): + print("File size:", config.stat().st_size) + else: + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text("[settings]\nname = 'Example'") + + +if __name__ == "__main__": + main() diff --git a/2025/standard/03_functools.py b/2025/standard/03_functools.py index e0942752..9e06259e 100644 --- a/2025/standard/03_functools.py +++ b/2025/standard/03_functools.py @@ -1,9 +1,11 @@ from functools import cache, partial + @cache def power(base: int, exponent: int) -> int: print(f"Computing {base}^{exponent}") - return base ** exponent + return base**exponent + def main() -> None: # cached power function @@ -19,5 +21,6 @@ def main() -> None: print("Cube of 2:", cube(2)) print("Square of 5 again (cached):", square(5)) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/standard/04_tomllib.py b/2025/standard/04_tomllib.py index 921c68d5..9abbc73a 100644 --- a/2025/standard/04_tomllib.py +++ b/2025/standard/04_tomllib.py @@ -1,6 +1,13 @@ import tomllib -with open("pyproject.toml", "rb") as f: - data = tomllib.load(f) -print(data["project"]["name"]) \ No newline at end of file +def main() -> None: + # Load a TOML file + with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + + print(data["project"]["name"]) + + +if __name__ == "__main__": + main() diff --git a/2025/standard/05_graphlib.py b/2025/standard/05_graphlib.py index b8a277ad..370461e3 100644 --- a/2025/standard/05_graphlib.py +++ b/2025/standard/05_graphlib.py @@ -2,7 +2,8 @@ ts = TopologicalSorter[str]() -def main(): + +def main() -> None: # Tasks and their dependencies ts.add("compile", "fetch_sources") ts.add("test", "compile") @@ -13,5 +14,6 @@ def main(): order = list(ts.static_order()) print("Execution order:", order) + if __name__ == "__main__": main() diff --git a/2025/standard/08_shutil.py b/2025/standard/08_shutil.py index 0140e7c0..5851bb47 100644 --- a/2025/standard/08_shutil.py +++ b/2025/standard/08_shutil.py @@ -1,13 +1,13 @@ -from py_compile import main import shutil -def main() -> None: +def main() -> None: # Copy a file shutil.copy("pyproject.toml", "backup_pyproject.toml") # Create a zip archive shutil.make_archive("project_backup", "zip", ".") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/standard/10_itertools.py b/2025/standard/10_itertools.py index 92375ff2..0a8b40eb 100644 --- a/2025/standard/10_itertools.py +++ b/2025/standard/10_itertools.py @@ -1,5 +1,5 @@ import itertools -from collections.abc import Iterable + def main() -> None: items = ["a", "b", "c"] @@ -28,5 +28,6 @@ def main() -> None: group_list = list(group) print(f"{key}: {group_list}") + if __name__ == "__main__": - main() \ No newline at end of file + main() From bac34ed06f2106a92cb82c37b49ad6c65139e866 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 15 Jul 2025 16:37:32 +0200 Subject: [PATCH 031/113] Added code example --- 2025/pydanticai/main.py | 101 +++ 2025/pydanticai/pyproject.toml | 10 + 2025/pydanticai/uv.lock | 1153 ++++++++++++++++++++++++++++++++ 3 files changed, 1264 insertions(+) create mode 100644 2025/pydanticai/main.py create mode 100644 2025/pydanticai/pyproject.toml create mode 100644 2025/pydanticai/uv.lock diff --git a/2025/pydanticai/main.py b/2025/pydanticai/main.py new file mode 100644 index 00000000..8e8bd92a --- /dev/null +++ b/2025/pydanticai/main.py @@ -0,0 +1,101 @@ +from dataclasses import dataclass +from typing import Any +import asyncio + +from pydantic import BaseModel, Field +from pydantic_ai import Agent, RunContext + +from dotenv import load_dotenv + + +# Load environment variables from .env +load_dotenv() + + +# Mock database +@dataclass +class Patient: + id: int + name: str + vitals: dict[str, Any] + +PATIENT_DB = { + 42: Patient(id=42, name="John Doe", vitals={"heart_rate": 72, "blood_pressure": "120/80"}), + 43: Patient(id=43, name="Jane Smith", vitals={"heart_rate": 65, "blood_pressure": "110/70"}), +} + +class DatabaseConn: + async def patient_name(self, id: int) -> str: + patient = PATIENT_DB.get(id) + return patient.name if patient else "Unknown Patient" + + async def latest_vitals(self, id: int) -> dict[str, Any]: + patient = PATIENT_DB.get(id) + return patient.vitals if patient else {"heart_rate": 0, "blood_pressure": "N/A"} + + +@dataclass +class TriageDependencies: + patient_id: int + db: DatabaseConn + + +class TriageOutput(BaseModel): + response_text: str = Field(description="Message to the patient") + escalate: bool = Field(description="Should escalate to a human nurse") + urgency: int = Field(description="Urgency level from 0 to 10", ge=0, le=10) + + +triage_agent = Agent( + "openai:gpt-4o", + deps_type=TriageDependencies, + output_type=TriageOutput, + system_prompt=( + "You are a triage assistant helping patients. " + "Provide clear advice and assess urgency." + ), +) + + +@triage_agent.system_prompt +async def add_patient_name(ctx: RunContext[TriageDependencies]) -> str: + patient_name = await ctx.deps.db.patient_name(id=ctx.deps.patient_id) + return f"The patient's name is {patient_name!r}." + + +@triage_agent.tool +async def latest_vitals(ctx: RunContext[TriageDependencies]) -> dict[str, Any]: + """Returns the patient's latest vital signs.""" + return await ctx.deps.db.latest_vitals(id=ctx.deps.patient_id) + + +async def main() -> None: + deps = TriageDependencies(patient_id=43, db=DatabaseConn()) + + result = await triage_agent.run( + "I have chest pain and trouble breathing.", + deps=deps, + ) + print(result.output) + """ + Example output: + response_text='Your symptoms are serious. Please call emergency services immediately. A nurse will contact you shortly.' + escalate=True + urgency=10 + """ + + result = await triage_agent.run( + "I have a mild headache since yesterday.", + deps=deps, + ) + print(result.output) + """ + Example output: + response_text='It sounds like your headache is not severe, but monitor it closely. If it worsens or you develop new symptoms, contact your doctor.' + escalate=False + urgency=3 + """ + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/2025/pydanticai/pyproject.toml b/2025/pydanticai/pyproject.toml new file mode 100644 index 00000000..3874920b --- /dev/null +++ b/2025/pydanticai/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "pydanticai" +version = "0.1.0" +description = "Pydantic AI tutorial" +requires-python = ">=3.13" +dependencies = [ + "pydantic>=2.11.7", + "pydantic-ai>=0.4.2", + "python-dotenv>=1.1.1", +] diff --git a/2025/pydanticai/uv.lock b/2025/pydanticai/uv.lock new file mode 100644 index 00000000..17df9ef6 --- /dev/null +++ b/2025/pydanticai/uv.lock @@ -0,0 +1,1153 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.57.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/75/6261a1a8d92aed47e27d2fcfb3a411af73b1435e6ae1186da02b760565d0/anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0", size = 423775, upload-time = "2025-07-03T16:57:35.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/cf/ca0ba77805aec6171629a8b665c7dc224dab374539c3d27005b5d8c100a0/anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306", size = 292779, upload-time = "2025-07-03T16:57:34.636Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "boto3" +version = "1.39.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/1f/b7510dcd26eb14735d6f4b2904e219b825660425a0cf0b6f35b84c7249b0/boto3-1.39.4.tar.gz", hash = "sha256:6c955729a1d70181bc8368e02a7d3f350884290def63815ebca8408ee6d47571", size = 111829, upload-time = "2025-07-09T19:23:01.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/5c/93292e4d8c809950c13950b3168e0eabdac828629c21047959251ad3f28c/boto3-1.39.4-py3-none-any.whl", hash = "sha256:f8e9534b429121aa5c5b7c685c6a94dd33edf14f87926e9a182d5b50220ba284", size = 139908, upload-time = "2025-07-09T19:22:59.808Z" }, +] + +[[package]] +name = "botocore" +version = "1.39.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/9f/21c823ea2fae3fa5a6c9e8caaa1f858acd55018e6d317505a4f14c5bb999/botocore-1.39.4.tar.gz", hash = "sha256:e662ac35c681f7942a93f2ec7b4cde8f8b56dd399da47a79fa3e370338521a56", size = 14136116, upload-time = "2025-07-09T19:22:49.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/44/f120319e0a9afface645e99f300175b9b308e4724cb400b32e1bd6eb3060/botocore-1.39.4-py3-none-any.whl", hash = "sha256:c41e167ce01cfd1973c3fa9856ef5244a51ddf9c82cb131120d8617913b6812a", size = 13795516, upload-time = "2025-07-09T19:22:44.446Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "cohere" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/c7/fd1e4c61cf3f0aac9d9d73fce63a766c9778e1270f7a26812eb289b4851d/cohere-5.16.1.tar.gz", hash = "sha256:02aa87668689ad0fbac2cda979c190310afdb99fb132552e8848fdd0aff7cd40", size = 162300, upload-time = "2025-07-09T20:47:36.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/c6/72309ac75f3567425ca31a601ad394bfee8d0f4a1569dfbc80cbb2890d07/cohere-5.16.1-py3-none-any.whl", hash = "sha256:37e2c1d69b1804071b5e5f5cb44f8b74127e318376e234572d021a1a729c6baa", size = 291894, upload-time = "2025-07-09T20:47:34.919Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, +] + +[[package]] +name = "fastavro" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/8f/32664a3245247b13702d13d2657ea534daf64e58a3f72a3a2d10598d6916/fastavro-1.11.1.tar.gz", hash = "sha256:bf6acde5ee633a29fb8dfd6dfea13b164722bc3adc05a0e055df080549c1c2f8", size = 1016250, upload-time = "2025-05-18T04:54:31.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/08/8e25b9e87a98f8c96b25e64565fa1a1208c0095bb6a84a5c8a4b925688a5/fastavro-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f963b8ddaf179660e814ab420850c1b4ea33e2ad2de8011549d958b21f77f20a", size = 931520, upload-time = "2025-05-18T04:55:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/7cf5561ef94781ed6942cee6b394a5e698080f4247f00f158ee396ec244d/fastavro-1.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0253e5b6a3c9b62fae9fc3abd8184c5b64a833322b6af7d666d3db266ad879b5", size = 3195989, upload-time = "2025-05-18T04:55:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/f02f097d79f090e5c5aca8a743010c4e833a257c0efdeb289c68294f7928/fastavro-1.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca637b150e1f4c0e8e564fad40a16bd922bcb7ffd1a6e4836e6084f2c4f4e8db", size = 3239755, upload-time = "2025-05-18T04:55:16.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/4c/46626b4ee4eb8eb5aa7835973c6ba8890cf082ef2daface6071e788d2992/fastavro-1.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76af1709031621828ca6ce7f027f7711fa33ac23e8269e7a5733996ff8d318da", size = 3243788, upload-time = "2025-05-18T04:55:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6f/8ed42524e9e8dc0554f0f211dd1c6c7a9dde83b95388ddcf7c137e70796f/fastavro-1.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8224e6d8d9864d4e55dafbe88920d6a1b8c19cc3006acfac6aa4f494a6af3450", size = 3378330, upload-time = "2025-05-18T04:55:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/b8/51/38cbe243d5facccab40fc43a4c17db264c261be955ce003803d25f0da2c3/fastavro-1.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:cde7ed91b52ff21f0f9f157329760ba7251508ca3e9618af3ffdac986d9faaa2", size = 443115, upload-time = "2025-05-18T04:55:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/d0/57/0d31ed1a49c65ad9f0f0128d9a928972878017781f9d4336f5f60982334c/fastavro-1.11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e5ed1325c1c414dd954e7a2c5074daefe1eceb672b8c727aa030ba327aa00693", size = 1021401, upload-time = "2025-05-18T04:55:23.431Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/a3f1a75fbfc16b3eff65dc0efcdb92364967923194312b3f8c8fc2cb95be/fastavro-1.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd3c95baeec37188899824faf44a5ee94dfc4d8667b05b2f867070c7eb174c4", size = 3384349, upload-time = "2025-05-18T04:55:25.575Z" }, + { url = "https://files.pythonhosted.org/packages/be/84/02bceb7518867df84027232a75225db758b9b45f12017c9743f45b73101e/fastavro-1.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e0babcd81acceb4c60110af9efa25d890dbb68f7de880f806dadeb1e70fe413", size = 3240658, upload-time = "2025-05-18T04:55:27.633Z" }, + { url = "https://files.pythonhosted.org/packages/f2/17/508c846c644d39bc432b027112068b8e96e7560468304d4c0757539dd73a/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c0cb8063c7208b53b6867983dc6ae7cc80b91116b51d435d2610a5db2fc52f", size = 3372809, upload-time = "2025-05-18T04:55:30.063Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/9c2917a70ed570ddbfd1d32ac23200c1d011e36c332e59950d2f6d204941/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1bc2824e9969c04ab6263d269a1e0e5d40b9bd16ade6b70c29d6ffbc4f3cc102", size = 3387171, upload-time = "2025-05-18T04:55:32.531Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "google-genai" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "google-auth" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/59/c9b9148c8702b60253f5a251f16ae436534c5d4362da193c9db05ac9858c/google_genai-1.25.0.tar.gz", hash = "sha256:a08a79c819a5d949d9948cd372e36e512bf85cd28158994daaa36d0ec4cb2b02", size = 228141, upload-time = "2025-07-09T20:53:47.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/ec/149f3d49b56cf848142071772aabb1c290b535bd9b5327a5dfccf1d00332/google_genai-1.25.0-py3-none-any.whl", hash = "sha256:fb5cee79b9a0a1b2afd5cfdf279099ecebd186551eefcaa6ec0c6016244e6138", size = 226847, upload-time = "2025-07-09T20:53:46.532Z" }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, +] + +[[package]] +name = "groq" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b1/72ca20dc9b977b7f604648e8944c77b267bddeb90d8e16bda0cf0e397844/groq-0.30.0.tar.gz", hash = "sha256:919466e48fcbebef08fed3f71debb0f96b0ea8d2ec77842c384aa843019f6e2c", size = 134928, upload-time = "2025-07-11T20:28:36.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/b8/5b90edf9fbd795597220e3d1b5534d845e69a73ffe1fdeb967443ed2a6cf/groq-0.30.0-py3-none-any.whl", hash = "sha256:6d9609a7778ba56432f45c1bac21b005f02c6c0aca9c1c094e65536f162c1e83", size = 131056, upload-time = "2025-07-11T20:28:35.591Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d4/7685999e85945ed0d7f0762b686ae7015035390de1161dcea9d5276c134c/hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694", size = 495969, upload-time = "2025-06-20T21:48:38.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/89/a1119eebe2836cb25758e7661d6410d3eae982e2b5e974bcc4d250be9012/hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23", size = 2687929, upload-time = "2025-06-20T21:48:32.284Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/2c78e28f309396e71ec8e4e9304a6483dcbc36172b5cea8f291994163425/hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8", size = 2556338, upload-time = "2025-06-20T21:48:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2f/6cad7b5fe86b7652579346cb7f85156c11761df26435651cbba89376cd2c/hf_xet-1.1.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc874b5c843e642f45fd85cda1ce599e123308ad2901ead23d3510a47ff506d1", size = 3102894, upload-time = "2025-06-20T21:48:28.114Z" }, + { url = "https://files.pythonhosted.org/packages/d0/54/0fcf2b619720a26fbb6cc941e89f2472a522cd963a776c089b189559447f/hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dbba1660e5d810bd0ea77c511a99e9242d920790d0e63c0e4673ed36c4022d18", size = 3002134, upload-time = "2025-06-20T21:48:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/1d351ac6cef7c4ba8c85744d37ffbfac2d53d0a6c04d2cabeba614640a78/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab34c4c3104133c495785d5d8bba3b1efc99de52c02e759cf711a91fd39d3a14", size = 3171009, upload-time = "2025-06-20T21:48:33.987Z" }, + { url = "https://files.pythonhosted.org/packages/c9/65/4b2ddb0e3e983f2508528eb4501288ae2f84963586fbdfae596836d5e57a/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:83088ecea236d5113de478acb2339f92c95b4fb0462acaa30621fac02f5a534a", size = 3279245, upload-time = "2025-06-20T21:48:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/f0/55/ef77a85ee443ae05a9e9cba1c9f0dd9241eb42da2aeba1dc50f51154c81a/hf_xet-1.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:73e167d9807d166596b4b2f0b585c6d5bd84a26dea32843665a8b58f6edba245", size = 2738931, upload-time = "2025-06-20T21:48:39.482Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.33.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/9e/9366b7349fc125dd68b9d384a0fea84d67b7497753fe92c71b67e13f47c4/huggingface_hub-0.33.4.tar.gz", hash = "sha256:6af13478deae120e765bfd92adad0ae1aec1ad8c439b46f23058ad5956cbca0a", size = 426674, upload-time = "2025-07-11T12:32:48.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/7b/98daa50a2db034cab6cd23a3de04fa2358cb691593d28e9130203eb7a805/huggingface_hub-0.33.4-py3-none-any.whl", hash = "sha256:09f9f4e7ca62547c70f8b82767eefadd2667f4e116acba2e3e62a5a81815a7bb", size = 515339, upload-time = "2025-07-11T12:32:46.346Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "logfire-api" +version = "3.24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9c/48de4d1869a3266d5d333fa699020b6197efe4c1b214a6411874c39aa708/logfire_api-3.24.2.tar.gz", hash = "sha256:13c913916d1f627b7155c898eac232bf60d9b927706b8fae05cbaf803ec1e9e5", size = 50767, upload-time = "2025-07-14T16:04:34.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/45/37fb3a69ab24ede522304bad4d04dff94fbeb606df2b7a63062f634ee9b2/logfire_api-3.24.2-py3-none-any.whl", hash = "sha256:855fdffd9f1f20d054ac12064a773e0fd80f706dfcced8775a429c05dfade89a", size = 85381, upload-time = "2025-07-14T16:04:30.838Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mcp" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistralai" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e7/204a54d07c37ebf173590af85bf46cddf8bc343b9d6005804581967b4751/mistralai-1.9.2.tar.gz", hash = "sha256:c0c6d5aff18ffccbc0d22c06fbc84280d71eeaeb08fa4e1ef7326b36629cfb0b", size = 192678, upload-time = "2025-07-10T13:07:08.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/eb/f746a3f977d3c0059e4afa19d26b1293f54c6258fcf841957e584be6927f/mistralai-1.9.2-py3-none-any.whl", hash = "sha256:7c3fff00e50227d379dea82052455c2610612a8ef476fa97393191aeeb7ab15f", size = 411581, upload-time = "2025-07-10T13:07:07.226Z" }, +] + +[[package]] +name = "openai" +version = "1.95.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/a3/70cd57c7d71086c532ce90de5fdef4165dc6ae9dbf346da6737ff9ebafaa/openai-1.95.1.tar.gz", hash = "sha256:f089b605282e2a2b6776090b4b46563ac1da77f56402a222597d591e2dcc1086", size = 488271, upload-time = "2025-07-11T20:47:24.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1d/0432ea635097f4dbb34641a3650803d8a4aa29d06bafc66583bf1adcceb4/openai-1.95.1-py3-none-any.whl", hash = "sha256:8bbdfeceef231b1ddfabbc232b179d79f8b849aab5a7da131178f8d10e0f162f", size = 755613, upload-time = "2025-07-11T20:47:22.629Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-ai" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "mcp", "mistral", "openai", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/c4/697c3f35d539f244d9cf0535d835547151eb0cde9ba74c87db3ee46a92df/pydantic_ai-0.4.2.tar.gz", hash = "sha256:3db0461ff9e5b383f37db1bf4645ccc3a82d3391bef0d42ed78a16930d7b9a71", size = 41347323, upload-time = "2025-07-10T18:53:46.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/02/eccaeb0f04b00cd19f9b6d0ba52071e6f864c7bd12aa5f00624456e36117/pydantic_ai-0.4.2-py3-none-any.whl", hash = "sha256:2335aed33b84769666087aa85852c191ae0cdbd47b13765bc5a2d1370b5a5d73", size = 10139, upload-time = "2025-07-10T18:53:35.216Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "griffe" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/27/463ed1dc0d02dc9984c74eb8fe423dd33fef6f4c93ce66f33630f0f71ebf/pydantic_ai_slim-0.4.2.tar.gz", hash = "sha256:31430bbe61bc1c3a7f212eb99b50cf18399c7842d4a51c3ffc1ea973f644a99b", size = 165984, upload-time = "2025-07-10T18:53:50.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6b/08c23ac8fcfa8ed2ccf564c51828e5f612e3e20cc86313e36e9b7ec48a41/pydantic_ai_slim-0.4.2-py3-none-any.whl", hash = "sha256:1dbbf31066b68b9e3cbb391e62114b620f02736607b90e64bccc3aa0e8f30475", size = 218913, upload-time = "2025-07-10T18:53:40.213Z" }, +] + +[package.optional-dependencies] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai" }, +] +groq = [ + { name = "groq" }, +] +mcp = [ + { name = "mcp" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/76/f71365828e36a18e12da940d52ca52f883cffde64cbb92aa8de1603bd2a0/pydantic_evals-0.4.2.tar.gz", hash = "sha256:f2f48a47548bbfd5a3a86c54b22a37f4d633d161a19e0948e2c5c2319d486c8f", size = 43281, upload-time = "2025-07-10T18:53:52.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/d3/f00d959b7fd56c7abe95ec195fab791de4c5b6c236f36affc78a4eb9072a/pydantic_evals-0.4.2-py3-none-any.whl", hash = "sha256:1486fc25d616928763c0a7a07c94f6a3ead8e922ac746494a13a799e87d3d473", size = 52023, upload-time = "2025-07-10T18:53:42.023Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/31/fb922d6c0cc19ad4da3cf6ecfc8f763c6b10d86f71ac7590e4b7d5e7c8fb/pydantic_graph-0.4.2.tar.gz", hash = "sha256:81339ff4e376b6149a4d548052f76019d0b969288f19d8dd4963ba854a18201c", size = 21868, upload-time = "2025-07-10T18:53:52.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/70/444c5311cdcc7d6f16c0921fee0bb82c54717f7d7cc600c14d72aeac9cdf/pydantic_graph-0.4.2-py3-none-any.whl", hash = "sha256:6a89fa4a8472c468e39843ad9ce9eaef79cdc8318e6bac868baff2bc7adf09b2", size = 27494, upload-time = "2025-07-10T18:53:43.795Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pydanticai" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, + { name = "pydantic-ai" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-ai", specifier = ">=0.4.2" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" }, + { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" }, + { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" }, + { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/3e/eae74d8d33e3262bae0a7e023bb43d8bdd27980aa3557333f4632611151f/sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926", size = 18635, upload-time = "2025-07-06T09:41:33.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f1/6c7eaa8187ba789a6dd6d74430307478d2a91c23a5452ab339b6fbe15a08/sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a", size = 10824, upload-time = "2025-07-06T09:41:32.321Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From bec49bf7640cbffbe948da981619464e4b81f0f2 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 8 Jul 2025 15:14:42 +0200 Subject: [PATCH 032/113] Added weather example --- 2025/testtips/pyproject.toml | 12 +++ 2025/testtips/uv.lock | 177 +++++++++++++++++++++++++++++++++++ 2025/testtips/weather.py | 32 +++++++ 3 files changed, 221 insertions(+) create mode 100644 2025/testtips/pyproject.toml create mode 100644 2025/testtips/uv.lock create mode 100644 2025/testtips/weather.py diff --git a/2025/testtips/pyproject.toml b/2025/testtips/pyproject.toml new file mode 100644 index 00000000..7a3d9a70 --- /dev/null +++ b/2025/testtips/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "testtips" +version = "0.0.1" +authors = [{name = "ArjanCodes"}] +license = {text = "MIT"} +requires-python = ">=3.12" +dependencies = [ + "httpx>=0.28.1", + "pytest>=8.4.1", + "python-dotenv>=1.1.1", +] + diff --git a/2025/testtips/uv.lock b/2025/testtips/uv.lock new file mode 100644 index 00000000..c24767b9 --- /dev/null +++ b/2025/testtips/uv.lock @@ -0,0 +1,177 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "testtips" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] diff --git a/2025/testtips/weather.py b/2025/testtips/weather.py new file mode 100644 index 00000000..5ad99161 --- /dev/null +++ b/2025/testtips/weather.py @@ -0,0 +1,32 @@ +# weather_service.py +import os + +import httpx +from dotenv import load_dotenv + +load_dotenv() + + +class WeatherService: + def __init__(self, api_key: str): + self.api_key = api_key + + def get_temperature(self, city: str) -> float: + response = httpx.get( + "https://api.weatherapi.com/v1/current.json", + params={"key": self.api_key, "q": city}, + ) + response.raise_for_status() + data = response.json() + return data["current"]["temp_c"] + + +def main(): + api_key = os.getenv("WEATHER_API_KEY") + weather_service = WeatherService(api_key) + temperature = weather_service.get_temperature("Amsterdam") + print(f"The current temperature in Amsterdam is {temperature}ยฐC.") + + +if __name__ == "__main__": + main() From 5ba29f1b69e1c60d9d7f12f0aa58ab5eba3958ba Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 11 Jul 2025 16:52:30 +0200 Subject: [PATCH 033/113] Added testing code examples. --- 2025/testtips/pyproject.toml | 4 + 2025/testtips/tests/test_tips.py | 73 ++++++++++++++++++ 2025/testtips/tests/test_weather.py | 78 ++++++++++++++++++++ 2025/testtips/tests/test_weather_fixture.py | 50 +++++++++++++ 2025/testtips/tests/test_weather_mock_mp.py | 41 ++++++++++ 2025/testtips/tests/test_weather_refactor.py | 12 +++ 2025/testtips/uv.lock | 33 +++++++++ 2025/testtips/weather_refactor.py | 34 +++++++++ 8 files changed, 325 insertions(+) create mode 100644 2025/testtips/tests/test_tips.py create mode 100644 2025/testtips/tests/test_weather.py create mode 100644 2025/testtips/tests/test_weather_fixture.py create mode 100644 2025/testtips/tests/test_weather_mock_mp.py create mode 100644 2025/testtips/tests/test_weather_refactor.py create mode 100644 2025/testtips/weather_refactor.py diff --git a/2025/testtips/pyproject.toml b/2025/testtips/pyproject.toml index 7a3d9a70..9720917a 100644 --- a/2025/testtips/pyproject.toml +++ b/2025/testtips/pyproject.toml @@ -6,7 +6,11 @@ license = {text = "MIT"} requires-python = ">=3.12" dependencies = [ "httpx>=0.28.1", + "hypothesis>=6.135.26", "pytest>=8.4.1", "python-dotenv>=1.1.1", ] +[tool.pytest.ini_options] +pythonpath = "." + diff --git a/2025/testtips/tests/test_tips.py b/2025/testtips/tests/test_tips.py new file mode 100644 index 00000000..2d938638 --- /dev/null +++ b/2025/testtips/tests/test_tips.py @@ -0,0 +1,73 @@ +# tests/test_weather.py + +import sys + +import pytest +from weather import WeatherService + +# from hypothesis import given +# from hypothesis.strategies import floats + + +# โœ… Parametrization +@pytest.mark.parametrize( + "city,expected_temp", [("London", 15), ("Berlin", 20), ("Paris", 17)] +) +def test_get_temperature_multiple_cities(monkeypatch, city, expected_temp): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": expected_temp}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + temp = service.get_temperature(city) + + assert temp == expected_temp + + +# โœ… pytest.raises +def test_get_temperature_raises_http_error(monkeypatch): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + raise Exception("API error") + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + + with pytest.raises(Exception): + service.get_temperature("Tokyo") + + +# โœ… pytest.mark.skip +@pytest.mark.skip(reason="Skipping this test for demonstration purposes.") +def test_skipped_example(): + assert False + + +# โœ… pytest.mark.skipif +@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on Windows") +def test_only_runs_on_non_windows(): + assert True + + +# โœ… pytest.mark.xfail +@pytest.mark.xfail(reason="Known bug: API sometimes returns wrong temperature") +def test_expected_failure_example(): + assert 2 + 2 == 5 + + +# โœ… Hypothesis property-based test +# @given(floats(min_value=-50, max_value=50)) +# def test_temperature_range_property(temp): +# assert -50 <= temp <= 50 diff --git a/2025/testtips/tests/test_weather.py b/2025/testtips/tests/test_weather.py new file mode 100644 index 00000000..542a6248 --- /dev/null +++ b/2025/testtips/tests/test_weather.py @@ -0,0 +1,78 @@ +import httpx +import pytest +from weather import WeatherService + + +# Tip 1: Single assert per test +def test_get_temperature_returns_expected_value(monkeypatch): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": 22}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Amsterdam") + + assert temp == 22 + + +# Tip 2: Clear and descriptive names +def test_get_temperature_for_different_city(monkeypatch): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": 18}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Berlin") + + assert temp == 18 + + +def test_get_temperature_handles_api_error(monkeypatch): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + raise httpx.HTTPError("API error") + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + + with pytest.raises(httpx.HTTPError): + service.get_temperature("Paris") + + +def test_get_temperature_returns_float(monkeypatch): + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": 19.5}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Rome") + + assert isinstance(temp, float) diff --git a/2025/testtips/tests/test_weather_fixture.py b/2025/testtips/tests/test_weather_fixture.py new file mode 100644 index 00000000..5d88dd24 --- /dev/null +++ b/2025/testtips/tests/test_weather_fixture.py @@ -0,0 +1,50 @@ +import pytest +from weather import WeatherService + + +# This fixture creates a WeatherService instance you can reuse +@pytest.fixture +def weather_service(): + return WeatherService(api_key="fake-key") + + +def test_get_temperature_returns_expected_value(weather_service, monkeypatch): + """ + Test that get_temperature returns the correct temperature. + """ + + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": 20}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + temp = weather_service.get_temperature("Amsterdam") + assert temp == 20 + + +def test_get_temperature_returns_float(weather_service, monkeypatch): + """ + Test that get_temperature returns a float value. + """ + + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"current": {"temp_c": 18.5}} + + return FakeResponse() + + monkeypatch.setattr("weather.httpx.get", fake_get) + + temp = weather_service.get_temperature("Berlin") + assert isinstance(temp, float) diff --git a/2025/testtips/tests/test_weather_mock_mp.py b/2025/testtips/tests/test_weather_mock_mp.py new file mode 100644 index 00000000..b28340ae --- /dev/null +++ b/2025/testtips/tests/test_weather_mock_mp.py @@ -0,0 +1,41 @@ +from weather import WeatherService +from unittest.mock import patch, MagicMock + +# Section 4: Mocking with patch + MagicMock +def test_get_temperature_with_mocking(): + """ + Example of mocking httpx.get with a MagicMock. + """ + # Create a MagicMock response object + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"current": {"temp_c": 25}} + + # Patch httpx.get so it returns our mock_response + with patch("weather.httpx.get", return_value=mock_response) as mock_get: + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Paris") + + assert temp == 25 + mock_get.assert_called_once() + + +# Section 5: Monkey patching with monkeypatch fixture +def test_get_temperature_with_monkeypatch(monkeypatch): + """ + Example of monkeypatching httpx.get with a manual stub. + """ + + def fake_get(url, params): + class FakeResponse: + def raise_for_status(self): pass + def json(self): return {"current": {"temp_c": 19}} + return FakeResponse() + + # Monkeypatch httpx.get to use fake_get + monkeypatch.setattr("weather.httpx.get", fake_get) + + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Berlin") + + assert temp == 19 \ No newline at end of file diff --git a/2025/testtips/tests/test_weather_refactor.py b/2025/testtips/tests/test_weather_refactor.py new file mode 100644 index 00000000..26c362fe --- /dev/null +++ b/2025/testtips/tests/test_weather_refactor.py @@ -0,0 +1,12 @@ +from weather_refactor import WeatherService + +def test_get_temperature_with_stub_client(): + class StubClient: + def get(self, url, params): + class Response: + def raise_for_status(self): pass + def json(self): return {"current": {"temp_c": 18}} + return Response() + + service = WeatherService(client=StubClient(), api_key="fake_key") + assert service.get_temperature("Oslo") == 18 \ No newline at end of file diff --git a/2025/testtips/uv.lock b/2025/testtips/uv.lock index c24767b9..c6502d83 100644 --- a/2025/testtips/uv.lock +++ b/2025/testtips/uv.lock @@ -16,6 +16,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -71,6 +80,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "hypothesis" +version = "6.135.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/83/15c4e30561a0d8c8d076c88cb159187823d877118f34c851ada3b9b02a7b/hypothesis-6.135.26.tar.gz", hash = "sha256:73af0e46cd5039c6806f514fed6a3c185d91ef88b5a1577477099ddbd1a2e300", size = 454523, upload-time = "2025-07-05T04:59:45.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/78/db4fdc464219455f8dde90074660c3faf8429101b2d1299cac7d219e3176/hypothesis-6.135.26-py3-none-any.whl", hash = "sha256:fa237cbe2ae2c31d65f7230dcb866139ace635dcfec6c30dddf25974dd8ff4b9", size = 521517, upload-time = "2025-07-05T04:59:42.061Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -150,12 +172,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "testtips" version = "0.0.1" source = { virtual = "." } dependencies = [ { name = "httpx" }, + { name = "hypothesis" }, { name = "pytest" }, { name = "python-dotenv" }, ] @@ -163,6 +195,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, + { name = "hypothesis", specifier = ">=6.135.26" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "python-dotenv", specifier = ">=1.1.1" }, ] diff --git a/2025/testtips/weather_refactor.py b/2025/testtips/weather_refactor.py new file mode 100644 index 00000000..26dc1404 --- /dev/null +++ b/2025/testtips/weather_refactor.py @@ -0,0 +1,34 @@ +# weather_service.py +import os + +import httpx +from dotenv import load_dotenv + +load_dotenv() + + +class WeatherService: + def __init__(self, client: httpx.Client, api_key: str): + self.client = client + self.api_key = api_key + + def get_temperature(self, city: str) -> float: + response = self.client.get( + "https://api.weatherapi.com/v1/current.json", + params={"key": self.api_key, "q": city}, + ) + response.raise_for_status() + data = response.json() + return data["current"]["temp_c"] + + +def main(): + api_key = os.getenv("WEATHER_API_KEY", "") + client = httpx.Client() + weather_service = WeatherService(client, api_key) + temperature = weather_service.get_temperature("Amsterdam") + print(f"The current temperature in Amsterdam is {temperature}ยฐC.") + + +if __name__ == "__main__": + main() From f2ef78d58ca1369f2daa5372e90b6b298eb6ccd3 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 22 Jul 2025 16:09:57 +0200 Subject: [PATCH 034/113] Updated testtips code example --- 2025/testtips/tests/test_tips.py | 67 +++++++---------- 2025/testtips/tests/test_weather.py | 78 -------------------- 2025/testtips/tests/test_weather_fixture.py | 55 +++----------- 2025/testtips/tests/test_weather_mock.py | 34 +++++++++ 2025/testtips/tests/test_weather_mock_mp.py | 41 ---------- 2025/testtips/tests/test_weather_patch.py | 16 ++++ 2025/testtips/tests/test_weather_refactor.py | 28 ++++--- 2025/testtips/weather.py | 7 +- 2025/testtips/weather_refactor.py | 5 +- 9 files changed, 114 insertions(+), 217 deletions(-) delete mode 100644 2025/testtips/tests/test_weather.py create mode 100644 2025/testtips/tests/test_weather_mock.py delete mode 100644 2025/testtips/tests/test_weather_mock_mp.py create mode 100644 2025/testtips/tests/test_weather_patch.py diff --git a/2025/testtips/tests/test_tips.py b/2025/testtips/tests/test_tips.py index 2d938638..3cffefe1 100644 --- a/2025/testtips/tests/test_tips.py +++ b/2025/testtips/tests/test_tips.py @@ -1,73 +1,62 @@ -# tests/test_weather.py - import sys +from typing import Any import pytest -from weather import WeatherService -# from hypothesis import given -# from hypothesis.strategies import floats +from weather import WeatherService -# โœ… Parametrization @pytest.mark.parametrize( - "city,expected_temp", [("London", 15), ("Berlin", 20), ("Paris", 17)] + "city,expected_temp", + [ + ("London", 15), + ("Berlin", 20), + ("Rome", 18), + ], ) -def test_get_temperature_multiple_cities(monkeypatch, city, expected_temp): - def fake_get(url, params): +def test_parametrized_temperatures( + monkeypatch: pytest.MonkeyPatch, city: str, expected_temp: float +) -> None: + def fake_get(url: str, params: dict[str, Any]) -> Any: class FakeResponse: - def raise_for_status(self): + def raise_for_status(self) -> None: pass - def json(self): + def json(self) -> dict[str, Any]: return {"current": {"temp_c": expected_temp}} return FakeResponse() - monkeypatch.setattr("weather.httpx.get", fake_get) - + monkeypatch.setattr("httpx.get", fake_get) service = WeatherService(api_key="fake-key") - temp = service.get_temperature(city) + assert service.get_temperature(city) == expected_temp - assert temp == expected_temp - -# โœ… pytest.raises -def test_get_temperature_raises_http_error(monkeypatch): - def fake_get(url, params): +def test_temperature_raises_error(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get(url: str, params: dict[str, Any]) -> Any: class FakeResponse: - def raise_for_status(self): + def raise_for_status(self) -> None: raise Exception("API error") return FakeResponse() - monkeypatch.setattr("weather.httpx.get", fake_get) - + monkeypatch.setattr("httpx.get", fake_get) service = WeatherService(api_key="fake-key") with pytest.raises(Exception): - service.get_temperature("Tokyo") + service.get_temperature("Oslo") -# โœ… pytest.mark.skip -@pytest.mark.skip(reason="Skipping this test for demonstration purposes.") -def test_skipped_example(): +@pytest.mark.skip(reason="Temporarily skipping for demo purposes") +def test_skipped() -> None: assert False -# โœ… pytest.mark.skipif -@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on Windows") -def test_only_runs_on_non_windows(): +@pytest.mark.skipif(sys.platform == "win32", reason="Fails on Windows") +def test_non_windows_behavior() -> None: assert True -# โœ… pytest.mark.xfail -@pytest.mark.xfail(reason="Known bug: API sometimes returns wrong temperature") -def test_expected_failure_example(): - assert 2 + 2 == 5 - - -# โœ… Hypothesis property-based test -# @given(floats(min_value=-50, max_value=50)) -# def test_temperature_range_property(temp): -# assert -50 <= temp <= 50 +@pytest.mark.xfail(reason="Intentional failure due to API bug") +def test_expected_failure() -> None: + assert 1 + 1 == 3 diff --git a/2025/testtips/tests/test_weather.py b/2025/testtips/tests/test_weather.py deleted file mode 100644 index 542a6248..00000000 --- a/2025/testtips/tests/test_weather.py +++ /dev/null @@ -1,78 +0,0 @@ -import httpx -import pytest -from weather import WeatherService - - -# Tip 1: Single assert per test -def test_get_temperature_returns_expected_value(monkeypatch): - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - pass - - def json(self): - return {"current": {"temp_c": 22}} - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - service = WeatherService(api_key="fake-key") - temp = service.get_temperature("Amsterdam") - - assert temp == 22 - - -# Tip 2: Clear and descriptive names -def test_get_temperature_for_different_city(monkeypatch): - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - pass - - def json(self): - return {"current": {"temp_c": 18}} - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - service = WeatherService(api_key="fake-key") - temp = service.get_temperature("Berlin") - - assert temp == 18 - - -def test_get_temperature_handles_api_error(monkeypatch): - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - raise httpx.HTTPError("API error") - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - service = WeatherService(api_key="fake-key") - - with pytest.raises(httpx.HTTPError): - service.get_temperature("Paris") - - -def test_get_temperature_returns_float(monkeypatch): - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - pass - - def json(self): - return {"current": {"temp_c": 19.5}} - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - service = WeatherService(api_key="fake-key") - temp = service.get_temperature("Rome") - - assert isinstance(temp, float) diff --git a/2025/testtips/tests/test_weather_fixture.py b/2025/testtips/tests/test_weather_fixture.py index 5d88dd24..31c91726 100644 --- a/2025/testtips/tests/test_weather_fixture.py +++ b/2025/testtips/tests/test_weather_fixture.py @@ -1,50 +1,19 @@ import pytest from weather import WeatherService +from typing import Any +from unittest.mock import MagicMock - -# This fixture creates a WeatherService instance you can reuse @pytest.fixture -def weather_service(): +def weather_service(monkeypatch: pytest.MonkeyPatch) -> WeatherService: + def fake_get(url: str, params: dict[str, Any]) -> Any: + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"current": {"temp_c": 25}} + return mock_response + + monkeypatch.setattr("httpx.get", fake_get) return WeatherService(api_key="fake-key") +def test_fixture_usage(weather_service: WeatherService) -> None: + assert weather_service.get_temperature("Paris") == 25 -def test_get_temperature_returns_expected_value(weather_service, monkeypatch): - """ - Test that get_temperature returns the correct temperature. - """ - - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - pass - - def json(self): - return {"current": {"temp_c": 20}} - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - temp = weather_service.get_temperature("Amsterdam") - assert temp == 20 - - -def test_get_temperature_returns_float(weather_service, monkeypatch): - """ - Test that get_temperature returns a float value. - """ - - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): - pass - - def json(self): - return {"current": {"temp_c": 18.5}} - - return FakeResponse() - - monkeypatch.setattr("weather.httpx.get", fake_get) - - temp = weather_service.get_temperature("Berlin") - assert isinstance(temp, float) diff --git a/2025/testtips/tests/test_weather_mock.py b/2025/testtips/tests/test_weather_mock.py new file mode 100644 index 00000000..44abc7dd --- /dev/null +++ b/2025/testtips/tests/test_weather_mock.py @@ -0,0 +1,34 @@ +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from weather import WeatherService + + +def test_get_temperature_with_mocking_monkeypatch( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fake_get(url: str, params: dict[str, Any]) -> Any: + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"current": {"temp_c": 25}} + return mock_response + + monkeypatch.setattr("httpx.get", fake_get) + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Amsterdam") + assert temp == 25 + + +def test_get_temperature_with_mocking() -> None: + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"current": {"temp_c": 25}} + + with patch("httpx.get", return_value=mock_response) as mock_get: + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("London") + + assert temp == 25 + mock_get.assert_called_once() diff --git a/2025/testtips/tests/test_weather_mock_mp.py b/2025/testtips/tests/test_weather_mock_mp.py deleted file mode 100644 index b28340ae..00000000 --- a/2025/testtips/tests/test_weather_mock_mp.py +++ /dev/null @@ -1,41 +0,0 @@ -from weather import WeatherService -from unittest.mock import patch, MagicMock - -# Section 4: Mocking with patch + MagicMock -def test_get_temperature_with_mocking(): - """ - Example of mocking httpx.get with a MagicMock. - """ - # Create a MagicMock response object - mock_response = MagicMock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"current": {"temp_c": 25}} - - # Patch httpx.get so it returns our mock_response - with patch("weather.httpx.get", return_value=mock_response) as mock_get: - service = WeatherService(api_key="fake-key") - temp = service.get_temperature("Paris") - - assert temp == 25 - mock_get.assert_called_once() - - -# Section 5: Monkey patching with monkeypatch fixture -def test_get_temperature_with_monkeypatch(monkeypatch): - """ - Example of monkeypatching httpx.get with a manual stub. - """ - - def fake_get(url, params): - class FakeResponse: - def raise_for_status(self): pass - def json(self): return {"current": {"temp_c": 19}} - return FakeResponse() - - # Monkeypatch httpx.get to use fake_get - monkeypatch.setattr("weather.httpx.get", fake_get) - - service = WeatherService(api_key="fake-key") - temp = service.get_temperature("Berlin") - - assert temp == 19 \ No newline at end of file diff --git a/2025/testtips/tests/test_weather_patch.py b/2025/testtips/tests/test_weather_patch.py new file mode 100644 index 00000000..2e8aab86 --- /dev/null +++ b/2025/testtips/tests/test_weather_patch.py @@ -0,0 +1,16 @@ +from weather import WeatherService +import pytest +from typing import Any + +def test_get_temperature_with_monkeypatch(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get(url: str, params: dict[str, Any]) -> Any: + class FakeResponse: + def raise_for_status(self) -> None: pass + def json(self) -> dict[str, Any]: + return {"current": {"temp_c": 19}} + return FakeResponse() + + monkeypatch.setattr("httpx.get", fake_get) + service = WeatherService(api_key="fake-key") + temp = service.get_temperature("Amsterdam") + assert temp == 19 \ No newline at end of file diff --git a/2025/testtips/tests/test_weather_refactor.py b/2025/testtips/tests/test_weather_refactor.py index 26c362fe..53245807 100644 --- a/2025/testtips/tests/test_weather_refactor.py +++ b/2025/testtips/tests/test_weather_refactor.py @@ -1,12 +1,22 @@ +from unittest.mock import MagicMock + +import pytest + from weather_refactor import WeatherService -def test_get_temperature_with_stub_client(): - class StubClient: - def get(self, url, params): - class Response: - def raise_for_status(self): pass - def json(self): return {"current": {"temp_c": 18}} - return Response() - service = WeatherService(client=StubClient(), api_key="fake_key") - assert service.get_temperature("Oslo") == 18 \ No newline at end of file +@pytest.fixture +def weather_service() -> WeatherService: + mock_http_client = MagicMock() + mock_http_client.get.return_value = MagicMock( + **{ + "raise_for_status": lambda: None, + "json": lambda: {"current": {"temp_c": 17}}, + } + ) + return WeatherService(client=mock_http_client, api_key="fake-key") + + +def test_weather_service_with_mock_http_client(weather_service: WeatherService): + temp = weather_service.get_temperature("Amsterdam") + assert temp == 17 diff --git a/2025/testtips/weather.py b/2025/testtips/weather.py index 5ad99161..1f10f142 100644 --- a/2025/testtips/weather.py +++ b/2025/testtips/weather.py @@ -1,4 +1,3 @@ -# weather_service.py import os import httpx @@ -8,7 +7,7 @@ class WeatherService: - def __init__(self, api_key: str): + def __init__(self, api_key: str) -> None: self.api_key = api_key def get_temperature(self, city: str) -> float: @@ -21,8 +20,8 @@ def get_temperature(self, city: str) -> float: return data["current"]["temp_c"] -def main(): - api_key = os.getenv("WEATHER_API_KEY") +def main() -> None: + api_key = os.getenv("WEATHER_API_KEY", "") weather_service = WeatherService(api_key) temperature = weather_service.get_temperature("Amsterdam") print(f"The current temperature in Amsterdam is {temperature}ยฐC.") diff --git a/2025/testtips/weather_refactor.py b/2025/testtips/weather_refactor.py index 26dc1404..95944c86 100644 --- a/2025/testtips/weather_refactor.py +++ b/2025/testtips/weather_refactor.py @@ -1,4 +1,3 @@ -# weather_service.py import os import httpx @@ -8,7 +7,7 @@ class WeatherService: - def __init__(self, client: httpx.Client, api_key: str): + def __init__(self, client: httpx.Client, api_key: str) -> None: self.client = client self.api_key = api_key @@ -22,7 +21,7 @@ def get_temperature(self, city: str) -> float: return data["current"]["temp_c"] -def main(): +def main() -> None: api_key = os.getenv("WEATHER_API_KEY", "") client = httpx.Client() weather_service = WeatherService(client, api_key) From 1bae6e9ff0878fe3c0dd50099ae13fb54226647a Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 28 Jul 2025 16:17:46 +0200 Subject: [PATCH 035/113] Added initial ai design pattern code examples. --- 2025/aidesign/chain_of_responsibility.py | 168 +++++++++++++++++++++++ 2025/aidesign/observer.py | 131 ++++++++++++++++++ 2025/aidesign/pyproject.toml | 11 ++ 2025/aidesign/strategy.py | 117 ++++++++++++++++ 2025/aidesign/uv.lock | 100 ++++++++++++++ 5 files changed, 527 insertions(+) create mode 100644 2025/aidesign/chain_of_responsibility.py create mode 100644 2025/aidesign/observer.py create mode 100644 2025/aidesign/pyproject.toml create mode 100644 2025/aidesign/strategy.py create mode 100644 2025/aidesign/uv.lock diff --git a/2025/aidesign/chain_of_responsibility.py b/2025/aidesign/chain_of_responsibility.py new file mode 100644 index 00000000..b68ba29e --- /dev/null +++ b/2025/aidesign/chain_of_responsibility.py @@ -0,0 +1,168 @@ +import asyncio +import os +from dataclasses import dataclass +from typing import Optional + +from dotenv import load_dotenv +from pydantic import BaseModel +from pydantic_ai import Agent + +# Load env vars +load_dotenv() +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") + +# ---------------------------------- +# Shared Dependencies +# ---------------------------------- + + +@dataclass +class TravelDeps: + user_name: str + + +# ---------------------------------- +# Shared Data Passed Through the Chain +# ---------------------------------- + + +class TripContext(BaseModel): + destination: Optional[str] = None + from_city: Optional[str] = None + arrival_time: Optional[str] = None + hotel_name: Optional[str] = None + hotel_location: Optional[str] = None + + +# ---------------------------------- +# Step 1: Choose Destination +# ---------------------------------- + + +class DestinationOutput(BaseModel): + destination: str + + +destination_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=DestinationOutput, + system_prompt="You help users select an ideal travel destination based on their preferences.", +) + + +# ---------------------------------- +# Step 2: Plan Flight +# ---------------------------------- + + +class FlightPlan(BaseModel): + from_city: str + to_city: str + arrival_time: str + + +flight_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=FlightPlan, + system_prompt="Plan a realistic flight itinerary for a trip. Include origin city and arrival time.", +) + + +# ---------------------------------- +# Step 3: Recommend Hotel +# ---------------------------------- + + +class HotelOption(BaseModel): + name: str + location: str + price_per_night_usd: int + stars: int + + +hotel_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=HotelOption, + system_prompt="Suggest a good hotel near the arrival airport or city center. Consider time of arrival and convenience.", +) + + +# ---------------------------------- +# Step 4: Suggest Activities +# ---------------------------------- + + +class Activities(BaseModel): + personalized_for: str + top_activities: list[str] + + +activity_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=Activities, + system_prompt="Suggest local activities close to the hotel and suitable for arrival time (e.g., evening, morning).", +) + + +# ---------------------------------- +# Chain Execution Logic +# ---------------------------------- + + +async def plan_trip(user_input: str, deps: TravelDeps): + print(f"\n๐Ÿ‘ค {deps.user_name} says: {user_input}") + ctx = TripContext() + + # Step 1: Destination + dest_result = await destination_agent.run(prompt=user_input, deps=deps) + ctx.destination = dest_result.output.destination + print(f"๐Ÿ“ Destination: {ctx.destination}") + + # Step 2: Flight + flight_prompt = f"Plan a flight to {ctx.destination}." + flight_result = await flight_agent.run(prompt=flight_prompt, deps=deps) + ctx.from_city = flight_result.output.from_city + ctx.arrival_time = flight_result.output.arrival_time + print( + f"โœˆ๏ธ Flight: from {ctx.from_city} โ†’ {ctx.destination}, arriving at {ctx.arrival_time}" + ) + + # Step 3: Hotel + hotel_prompt = ( + f"Recommend a hotel in {ctx.destination} for a traveler arriving at {ctx.arrival_time}. " + f"Prefer locations near the airport or city center." + ) + hotel_result = await hotel_agent.run(prompt=hotel_prompt, deps=deps) + ctx.hotel_name = hotel_result.output.name + ctx.hotel_location = hotel_result.output.location + print( + f"๐Ÿจ Hotel: {ctx.hotel_name}, {hotel_result.output.stars}โ˜… at ${hotel_result.output.price_per_night_usd}/night" + ) + + # Step 4: Activities + activities_prompt = ( + f"Suggest activities in {ctx.destination} close to {ctx.hotel_location} " + f"and suitable for a traveler arriving at {ctx.arrival_time}." + ) + activity_result = await activity_agent.run(prompt=activities_prompt, deps=deps) + print(f"๐ŸŽฏ Activities for {activity_result.output.personalized_for}:") + for a in activity_result.output.top_activities: + print(f" - {a}") + + +# ---------------------------------- +# Main Execution +# ---------------------------------- + + +async def main(): + deps = TravelDeps(user_name="Maria") + await plan_trip("I want a quiet, sunny destination near the ocean.", deps) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2025/aidesign/observer.py b/2025/aidesign/observer.py new file mode 100644 index 00000000..590ead04 --- /dev/null +++ b/2025/aidesign/observer.py @@ -0,0 +1,131 @@ +import asyncio +import os +from dataclasses import dataclass + +from dotenv import load_dotenv +from pydantic import BaseModel, Field +from pydantic_ai import Agent + +# Load API key +load_dotenv() +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") + +# ------------------------------- +# Dependencies +# ------------------------------- + + +@dataclass +class TravelDeps: + user_name: str + + +# ------------------------------- +# Main Response Output +# ------------------------------- + + +class TravelResponse(BaseModel): + message: str = Field(..., description="Response to the user") + destination: str = Field(..., description="Suggested destination") + + +# ------------------------------- +# Structured Log Entry +# ------------------------------- + + +class LogEntry(BaseModel): + level: str = Field(..., description="Log level: info, warning, error, etc.") + message: str = Field(..., description="What the agent did") + source: str = Field( + ..., description="The part of the system that generated the log" + ) + + +# ------------------------------- +# Agent: Travel Recommender +# ------------------------------- + +travel_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=TravelResponse, + system_prompt="You are a helpful travel assistant. Recommend a good destination and respond politely.", +) + + +# ------------------------------- +# Log Agent: Parallel Output +# ------------------------------- + +log_agent = Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=LogEntry, + system_prompt="Log what the travel agent just did in a structured format.", +) + + +# ------------------------------- +# Observer Interface +# ------------------------------- + + +class Observer: + def notify(self, log: LogEntry): + pass + + +# ------------------------------- +# Example Concrete Observer +# ------------------------------- + + +class ConsoleLogger(Observer): + def notify(self, log: LogEntry): + print(f"[{log.level.upper()}] from {log.source}: {log.message}") + + +# ------------------------------- +# Execution Function +# ------------------------------- + + +async def recommend_travel( + user_prompt: str, deps: TravelDeps, observers: list[Observer] +): + # Step 1: Get the response from the agent + response_result = await travel_agent.run(user_prompt, deps=deps) + response = response_result.output + + # Step 2: Generate structured log from a separate agent + log_prompt = ( + f"The agent suggested {response.destination} in response to a user prompt." + ) + log_result = await log_agent.run(log_prompt, deps=deps) + log_entry = log_result.output + + # Step 3: Notify observers + for observer in observers: + observer.notify(log_entry) + + # Step 4: Show user response + print(f"\n๐Ÿ‘ค {deps.user_name} asked: {user_prompt}") + print(f"๐Ÿค– Travel Agent says: {response.message}") + print(f"๐Ÿ“ Destination Suggested: {response.destination}") + + +# ------------------------------- +# Main +# ------------------------------- + + +async def main(): + deps = TravelDeps(user_name="Alex") + observers = [ConsoleLogger()] + await recommend_travel("I want to go somewhere warm with beaches.", deps, observers) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2025/aidesign/pyproject.toml b/2025/aidesign/pyproject.toml new file mode 100644 index 00000000..e4d1804a --- /dev/null +++ b/2025/aidesign/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "aidesign" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "pydantic>=2.11.7", + "python-dotenv>=1.1.1", +] + +[tool.uv] +environments = ["sys_platform == 'darwin'"] \ No newline at end of file diff --git a/2025/aidesign/strategy.py b/2025/aidesign/strategy.py new file mode 100644 index 00000000..59f73c7b --- /dev/null +++ b/2025/aidesign/strategy.py @@ -0,0 +1,117 @@ +import asyncio +import os +from dataclasses import dataclass +from typing import Callable + +from dotenv import load_dotenv +from pydantic import BaseModel, Field +from pydantic_ai import Agent + +# Load API key +load_dotenv() +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") + +# ---------------------------------- +# Common Output Model +# ---------------------------------- + + +class TravelRecommendation(BaseModel): + destination: str = Field(..., description="Recommended destination") + message: str = Field(..., description="Message from the travel agent to the user") + + +# ---------------------------------- +# Dependencies (for extensibility) +# ---------------------------------- + + +@dataclass +class TravelDeps: + user_name: str + + +# ---------------------------------- +# Strategy Functions +# Each strategy returns an Agent with a specific personality +# ---------------------------------- + + +def professional_agent() -> Agent[TravelDeps, TravelRecommendation]: + return Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=TravelRecommendation, + system_prompt=( + "You are a highly professional and polite travel agent. " + "You give thoughtful recommendations based on user preferences." + ), + ) + + +def fun_agent() -> Agent[TravelDeps, TravelRecommendation]: + return Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=TravelRecommendation, + system_prompt=( + "You are a fun, quirky travel agent who gets super excited about cool places. " + "Your responses are friendly and humorous, but still helpful." + ), + ) + + +def budget_agent() -> Agent[TravelDeps, TravelRecommendation]: + return Agent( + "openai:gpt-4o", + deps_type=TravelDeps, + output_type=TravelRecommendation, + system_prompt=( + "You are a frugal travel expert who finds great destinations with low cost. " + "Your suggestions should highlight affordability and value." + ), + ) + + +# ---------------------------------- +# Function to Run Strategy +# ---------------------------------- + + +async def run_travel_strategy( + user_prompt: str, + deps: TravelDeps, + strategy_fn: Callable[[], Agent[TravelDeps, TravelRecommendation]], +): + agent = strategy_fn() + result = await agent.run(user_prompt, deps=deps) + output = result.output + + # Display the structured response + print(f"\n๐Ÿ‘ค {deps.user_name} asked: {user_prompt}") + print(f"๐Ÿค– {agent.model}: {output.message}") + print(f"๐Ÿ“ Destination: {output.destination}") + + +# ---------------------------------- +# Run Example with Different Strategies +# ---------------------------------- + + +async def main(): + deps = TravelDeps(user_name="Sam") + + print("=== Professional Agent ===") + await run_travel_strategy( + "I want a peaceful place by the sea.", deps, professional_agent + ) + + print("\n=== Fun Agent ===") + await run_travel_strategy("I want a peaceful place by the sea.", deps, fun_agent) + + print("\n=== Budget Agent ===") + await run_travel_strategy("I want a peaceful place by the sea.", deps, budget_agent) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2025/aidesign/uv.lock b/2025/aidesign/uv.lock new file mode 100644 index 00000000..93a620c1 --- /dev/null +++ b/2025/aidesign/uv.lock @@ -0,0 +1,100 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "aidesign" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] From 1af31f6b8b7a110d94009220e07bde8111764d3b Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 28 Jul 2025 23:58:45 +0200 Subject: [PATCH 036/113] Added pydantic ai dependency --- 2025/aidesign/pyproject.toml | 3 +- 2025/aidesign/uv.lock | 1082 +++++++++++++++++++++++++++++++++- 2 files changed, 1062 insertions(+), 23 deletions(-) diff --git a/2025/aidesign/pyproject.toml b/2025/aidesign/pyproject.toml index e4d1804a..2b7681ae 100644 --- a/2025/aidesign/pyproject.toml +++ b/2025/aidesign/pyproject.toml @@ -4,8 +4,9 @@ version = "0.1.0" requires-python = ">=3.13" dependencies = [ "pydantic>=2.11.7", + "pydantic-ai>=0.4.8", "python-dotenv>=1.1.1", ] [tool.uv] -environments = ["sys_platform == 'darwin'"] \ No newline at end of file +environments = ["sys_platform == 'darwin'"] diff --git a/2025/aidesign/uv.lock b/2025/aidesign/uv.lock index 93a620c1..4805e73d 100644 --- a/2025/aidesign/uv.lock +++ b/2025/aidesign/uv.lock @@ -1,22 +1,83 @@ version = 1 revision = 2 requires-python = ">=3.13" +resolution-markers = [ + "sys_platform == 'darwin'", +] +supported-markers = [ + "sys_platform == 'darwin'", +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/de/0bddf7f26d5f38274c99401735c82ad59df9cead6de42f4bb2ad837286fe/ag_ui_protocol-0.1.8.tar.gz", hash = "sha256:eb745855e9fc30964c77e953890092f8bd7d4bbe6550d6413845428dd0faac0b", size = 5323, upload-time = "2025-07-15T10:55:36.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/00/40c6b0313c25d1ab6fac2ecba1cd5b15b1cd3c3a71b3d267ad890e405889/ag_ui_protocol-0.1.8-py3-none-any.whl", hash = "sha256:1567ccb067b7b8158035b941a985e7bb185172d660d4542f3f9c6fff77b55c6e", size = 7066, upload-time = "2025-07-15T10:55:35.075Z" }, +] [[package]] name = "aidesign" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-ai", marker = "sys_platform == 'darwin'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin'" }, ] [package.metadata] requires-dist = [ { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-ai", specifier = ">=0.4.8" }, { name = "python-dotenv", specifier = ">=1.1.1" }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin'" }, + { name = "aiosignal", marker = "sys_platform == 'darwin'" }, + { name = "attrs", marker = "sys_platform == 'darwin'" }, + { name = "frozenlist", marker = "sys_platform == 'darwin'" }, + { name = "multidict", marker = "sys_platform == 'darwin'" }, + { name = "propcache", marker = "sys_platform == 'darwin'" }, + { name = "yarl", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -26,47 +87,767 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "distro", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "jiter", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "sniffio", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/03/3334921dc54ed822b3dd993ae72d823a7402588521bbba3e024b3333a1fd/anthropic-0.60.0.tar.gz", hash = "sha256:a22ba187c6f4fd5afecb2fc913b960feccf72bc0d25c1b7ce0345e87caede577", size = 425983, upload-time = "2025-07-28T19:53:47.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/bb/d84f287fb1c217b30c328af987cf8bbe3897edf0518dcc5fa39412f794ec/anthropic-0.60.0-py3-none-any.whl", hash = "sha256:65ad1f088a960217aaf82ba91ff743d6c89e9d811c6d64275b9a7c59ee9ac3c6", size = 293116, upload-time = "2025-07-28T19:53:45.944Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin'" }, + { name = "sniffio", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "boto3" +version = "1.39.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore", marker = "sys_platform == 'darwin'" }, + { name = "jmespath", marker = "sys_platform == 'darwin'" }, + { name = "s3transfer", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/65/ddd4f52d138e52c1345c2d2421281a98449a6e4365290477befe06fa649a/boto3-1.39.15.tar.gz", hash = "sha256:b4483625f0d8c35045254dee46cd3c851bbc0450814f20b9b25bee1b5c0d8409", size = 111856, upload-time = "2025-07-28T19:56:49.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c5/27f50a31317041dc3ad79d62f37d5fcfb3f349c2fba8ea3e81de169db870/boto3-1.39.15-py3-none-any.whl", hash = "sha256:38fc54576b925af0075636752de9974e172c8a2cf7133400e3e09b150d20fb6a", size = 139901, upload-time = "2025-07-28T19:56:47.381Z" }, +] + +[[package]] +name = "botocore" +version = "1.39.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath", marker = "sys_platform == 'darwin'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin'" }, + { name = "urllib3", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/e2/8cd9560e7e44cf977dc0cc2e48da7634e78b7104ae6e47f4e1dfc1093965/botocore-1.39.15.tar.gz", hash = "sha256:2aa29a717f14f8c7ca058c2e297aaed0aa10ecea24b91514eee802814d1b7600", size = 14237556, upload-time = "2025-07-28T19:56:39.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/6e/f25b8633e7ab2008de4c27466c9bc39e32dc73816619ffebbea12936135a/botocore-1.39.15-py3-none-any.whl", hash = "sha256:eb9cfe918ebfbfb8654e1b153b29f0c129d586d2c0d7fb4032731d49baf04cff", size = 13894884, upload-time = "2025-07-28T19:56:33.715Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "cohere" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "httpx-sse", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin'" }, + { name = "types-requests", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/c7/fd1e4c61cf3f0aac9d9d73fce63a766c9778e1270f7a26812eb289b4851d/cohere-5.16.1.tar.gz", hash = "sha256:02aa87668689ad0fbac2cda979c190310afdb99fb132552e8848fdd0aff7cd40", size = 162300, upload-time = "2025-07-09T20:47:36.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/c6/72309ac75f3567425ca31a601ad394bfee8d0f4a1569dfbc80cbb2890d07/cohere-5.16.1-py3-none-any.whl", hash = "sha256:37e2c1d69b1804071b5e5f5cb44f8b74127e318376e234572d021a1a729c6baa", size = 291894, upload-time = "2025-07-09T20:47:34.919Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, +] + +[[package]] +name = "fastavro" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/8f/32664a3245247b13702d13d2657ea534daf64e58a3f72a3a2d10598d6916/fastavro-1.11.1.tar.gz", hash = "sha256:bf6acde5ee633a29fb8dfd6dfea13b164722bc3adc05a0e055df080549c1c2f8", size = 1016250, upload-time = "2025-05-18T04:54:31.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/08/8e25b9e87a98f8c96b25e64565fa1a1208c0095bb6a84a5c8a4b925688a5/fastavro-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f963b8ddaf179660e814ab420850c1b4ea33e2ad2de8011549d958b21f77f20a", size = 931520, upload-time = "2025-05-18T04:55:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/d0/57/0d31ed1a49c65ad9f0f0128d9a928972878017781f9d4336f5f60982334c/fastavro-1.11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e5ed1325c1c414dd954e7a2c5074daefe1eceb672b8c727aa030ba327aa00693", size = 1021401, upload-time = "2025-05-18T04:55:23.431Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools", marker = "sys_platform == 'darwin'" }, + { name = "pyasn1-modules", marker = "sys_platform == 'darwin'" }, + { name = "rsa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "google-genai" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "google-auth", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, + { name = "tenacity", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, + { name = "websockets", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/37/6c0ececc3a7a629029b5beed2ceb9f28f73292236eb96272355636769b0d/google_genai-1.27.0.tar.gz", hash = "sha256:15a13ffe7b3938da50b9ab77204664d82122617256f55b5ce403d593848ef635", size = 220099, upload-time = "2025-07-23T22:00:46.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/12/279afe7357af73f9737a3412b6f0bc1482075b896340eb46a2f9cb0fd791/google_genai-1.27.0-py3-none-any.whl", hash = "sha256:afd6b4efaf8ec1d20a6e6657d768b68d998d60007c6e220e9024e23c913c1833", size = 218489, upload-time = "2025-07-23T22:00:44.879Z" }, +] + +[[package]] +name = "griffe" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/42/486d21a6c33ff69a7381511d507b6db7a7b7f4d5bec3279bc0dc45c658a9/griffe-1.9.0.tar.gz", hash = "sha256:b5531cf45e9b73f0842c2121cc4d4bcbb98a55475e191fc9830e7aef87a920a0", size = 409341, upload-time = "2025-07-28T17:45:38.712Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/65/7b3fcef8c9fb6d1023484d9caf87e78450a5c9cd1e191ce9632990b65284/griffe-1.9.0-py3-none-any.whl", hash = "sha256:bcf90ee3ad42bbae70a2a490c782fc8e443de9b84aa089d857c278a4e23215fc", size = 137060, upload-time = "2025-07-28T17:45:36.973Z" }, +] + +[[package]] +name = "groq" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "distro", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "sniffio", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b1/72ca20dc9b977b7f604648e8944c77b267bddeb90d8e16bda0cf0e397844/groq-0.30.0.tar.gz", hash = "sha256:919466e48fcbebef08fed3f71debb0f96b0ea8d2ec77842c384aa843019f6e2c", size = 134928, upload-time = "2025-07-11T20:28:36.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/b8/5b90edf9fbd795597220e3d1b5534d845e69a73ffe1fdeb967443ed2a6cf/groq-0.30.0-py3-none-any.whl", hash = "sha256:6d9609a7778ba56432f45c1bac21b005f02c6c0aca9c1c094e65536f162c1e83", size = 131056, upload-time = "2025-07-11T20:28:35.591Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d4/7685999e85945ed0d7f0762b686ae7015035390de1161dcea9d5276c134c/hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694", size = 495969, upload-time = "2025-06-20T21:48:38.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/89/a1119eebe2836cb25758e7661d6410d3eae982e2b5e974bcc4d250be9012/hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23", size = 2687929, upload-time = "2025-06-20T21:48:32.284Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/2c78e28f309396e71ec8e4e9304a6483dcbc36172b5cea8f291994163425/hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8", size = 2556338, upload-time = "2025-06-20T21:48:30.079Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin'" }, + { name = "h11", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "certifi", marker = "sys_platform == 'darwin'" }, + { name = "httpcore", marker = "sys_platform == 'darwin'" }, + { name = "idna", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "hf-xet", marker = "(platform_machine == 'aarch64' and sys_platform == 'darwin') or (platform_machine == 'amd64' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, + { name = "packaging", marker = "sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, + { name = "tqdm", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/53/572b9c03ca0cabb3d71e02b1750b595196332cfb8c4d74a90de383451171/huggingface_hub-0.34.2.tar.gz", hash = "sha256:a27c1ba3d2a70b378dce546c8be3a90349a64e6bd5d7a806679d4bf5e5d2d8fe", size = 456837, upload-time = "2025-07-28T10:12:09.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/20/5ee412acef0af05bd3ccc78186ccb7ca672f9998a7cbc94c011df8f101f4/huggingface_hub-0.34.2-py3-none-any.whl", hash = "sha256:699843fc58d3d257dbd3cb014e0cd34066a56372246674322ba0909981ec239c", size = 558843, upload-time = "2025-07-28T10:12:07.064Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp", marker = "sys_platform == 'darwin'" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'darwin'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'darwin'" }, + { name = "referencing", marker = "sys_platform == 'darwin'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/37/0ddb054e58b1b726f31390058f1a61440ce6383b6d0e8fd532aeb73e54f1/logfire_api-4.0.0.tar.gz", hash = "sha256:74a027693d5789d699c2b5f4c135e3e343faf1cc086a99cce894ff0d632b6165", size = 52094, upload-time = "2025-07-22T15:12:07.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/3b/9e49b11b0e9df8f224ac60849829bcf662c5894e681e9b5210e41094a47f/logfire_api-4.0.0-py3-none-any.whl", hash = "sha256:1b8ec5396d7327bc325235fe012e09f756ae6d9f631aa62a387d668669fb1bff", size = 87424, upload-time = "2025-07-22T15:12:04.099Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mcp" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "httpx-sse", marker = "sys_platform == 'darwin'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin'" }, + { name = "python-multipart", marker = "sys_platform == 'darwin'" }, + { name = "sse-starlette", marker = "sys_platform == 'darwin'" }, + { name = "starlette", marker = "sys_platform == 'darwin'" }, + { name = "uvicorn", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/85/f36d538b1286b7758f35c1b69d93f2719d2df90c01bd074eadd35f6afc35/mcp-1.12.2.tar.gz", hash = "sha256:a4b7c742c50ce6ed6d6a6c096cca0e3893f5aecc89a59ed06d47c4e6ba41edcc", size = 426202, upload-time = "2025-07-24T18:29:05.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/cf/3fd38cfe43962452e4bfadc6966b2ea0afaf8e0286cb3991c247c8c33ebd/mcp-1.12.2-py3-none-any.whl", hash = "sha256:b86d584bb60193a42bd78aef01882c5c42d614e416cbf0480149839377ab5a5f", size = 158473, upload-time = "2025-07-24T18:29:03.419Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistralai" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/1d/280c6582124ff4aab3009f0c0282fd48e7fa3a60457f25e9196dc3cc2b8f/mistralai-1.9.3.tar.gz", hash = "sha256:a69806247ed3a67820ecfc9a68b7dbc0c6120dad5e5c3d507bd57fa388b491b7", size = 197355, upload-time = "2025-07-23T19:12:16.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/9a/0c48706c646b0391b798f8568f2b1545e54d345805e988003c10450b7b4c/mistralai-1.9.3-py3-none-any.whl", hash = "sha256:962445e7cebadcbfbcd1daf973e853a832dcf7aba6320468fcf7e2cf5f943aec", size = 426266, upload-time = "2025-07-23T19:12:15.414Z" }, +] + +[[package]] +name = "multidict" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, +] + +[[package]] +name = "openai" +version = "1.97.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "distro", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "jiter", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "sniffio", marker = "sys_platform == 'darwin'" }, + { name = "tqdm", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/57/1c471f6b3efb879d26686d31582997615e969f3bb4458111c9705e56332e/openai-1.97.1.tar.gz", hash = "sha256:a744b27ae624e3d4135225da9b1c89c107a2a7e5bc4c93e5b7b5214772ce7a4e", size = 494267, upload-time = "2025-07-22T13:10:12.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/35/412a0e9c3f0d37c94ed764b8ac7adae2d834dbd20e69f6aca582118e0f55/openai-1.97.1-py3-none-any.whl", hash = "sha256:4e96bbdf672ec3d44968c9ea39d2c375891db1acc1794668d8149d5fa6000606", size = 764380, upload-time = "2025-07-22T13:10:10.689Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[[package]] +name = "pydantic-ai" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mcp", "mistral", "openai", "retries", "vertexai"], marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/44/e1f6527d145c6f4ad42124c6393d71f59a58a0cbe21fd5650aee264a4452/pydantic_ai-0.4.8.tar.gz", hash = "sha256:b0e01494e881c4f72cc8137e37512473de04540761a581e996aeb9707ee68ae4", size = 43552745, upload-time = "2025-07-28T15:03:12.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/79/44c2c876d91d5e8c99f023489fd5a3e9364130dd05ef8e32b31ca243933d/pydantic_ai-0.4.8-py3-none-any.whl", hash = "sha256:34080e8ca4f28028e4643603ad903c18563e6435371c4f54729ad1ab5f402795", size = 10184, upload-time = "2025-07-28T15:03:00.4Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "sys_platform == 'darwin'" }, + { name = "griffe", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-graph", marker = "sys_platform == 'darwin'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/74/38e4cd2cfd9a621bb9f5991ff5f23dfdfaf3b86e3c176ca2b501155ff006/pydantic_ai_slim-0.4.8.tar.gz", hash = "sha256:0852b1635caaf97e67bff8a8633f45ffe3265d5231f0bf261ec9e422d1222b31", size = 189216, upload-time = "2025-07-28T15:03:16.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/3e/a7c93524eb5b8f48596e0ddc4848328145974dce12e641a87f7035909041/pydantic_ai_slim-0.4.8-py3-none-any.whl", hash = "sha256:34f50350714247bd0a1e8af4bb07c2478f14ab09cd9e01d4a2a9897ff7af00bb", size = 254430, upload-time = "2025-07-28T15:03:04.543Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol", marker = "sys_platform == 'darwin'" }, + { name = "starlette", marker = "sys_platform == 'darwin'" }, +] +anthropic = [ + { name = "anthropic", marker = "sys_platform == 'darwin'" }, +] +bedrock = [ + { name = "boto3", marker = "sys_platform == 'darwin'" }, +] +cli = [ + { name = "argcomplete", marker = "sys_platform == 'darwin'" }, + { name = "prompt-toolkit", marker = "sys_platform == 'darwin'" }, + { name = "rich", marker = "sys_platform == 'darwin'" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform == 'darwin'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin'" }, +] +evals = [ + { name = "pydantic-evals", marker = "sys_platform == 'darwin'" }, +] +google = [ + { name = "google-genai", marker = "sys_platform == 'darwin'" }, +] +groq = [ + { name = "groq", marker = "sys_platform == 'darwin'" }, +] +huggingface = [ + { name = "huggingface-hub", extra = ["inference"], marker = "sys_platform == 'darwin'" }, +] +mcp = [ + { name = "mcp", marker = "sys_platform == 'darwin'" }, +] +mistral = [ + { name = "mistralai", marker = "sys_platform == 'darwin'" }, +] +openai = [ + { name = "openai", marker = "sys_platform == 'darwin'" }, +] +retries = [ + { name = "tenacity", marker = "sys_platform == 'darwin'" }, +] +vertexai = [ + { name = "google-auth", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, + { name = "logfire-api", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "pydantic-ai-slim", marker = "sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin'" }, + { name = "rich", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/2c/5927176e27dbb73592684e48ae26701a96e36e944dde023e860573659226/pydantic_evals-0.4.8.tar.gz", hash = "sha256:a2f265d4bdaab6fb1f13fa8b99590ba48086d0214d8ffc544bf2c8400ca7ba5d", size = 43729, upload-time = "2025-07-28T15:03:17.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b1/353bc5050ef6cb075dada0ba00e42ea0d5d5e2123406f4e71046b11cebeb/pydantic_evals-0.4.8-py3-none-any.whl", hash = "sha256:7e05f79f13c4a2b146090c0c031765ec5a658f96059c541335b3fd4708275ad3", size = 52505, upload-time = "2025-07-28T15:03:06.678Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "logfire-api", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a4/7805229a07bf32cb4545b4c942e3a8ac570a4a0b91a0010237fb43217467/pydantic_graph-0.4.8.tar.gz", hash = "sha256:78a1d8ab084cfbbde418f901296d7e0b37299c5182de0b16efc1fd0bd173795d", size = 21980, upload-time = "2025-07-28T15:03:18.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/e1/fa5dd1a98350790e2f952ea9778f09f1fd6be31eee885b5c096514ff535e/pydantic_graph-0.4.8-py3-none-any.whl", hash = "sha256:6e7282d07896d96c7678bcf17cef7643718b05cda15024a11e8a22e3b5dfb42f", size = 27566, upload-time = "2025-07-28T15:03:08.572Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] @@ -78,6 +859,191 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'darwin'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin'" }, + { name = "charset-normalizer", marker = "sys_platform == 'darwin'" }, + { name = "idna", marker = "sys_platform == 'darwin'" }, + { name = "urllib3", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "sys_platform == 'darwin'" }, + { name = "pygments", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1" @@ -92,9 +1058,81 @@ name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin'" }, + { name = "h11", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin'" }, + { name = "multidict", marker = "sys_platform == 'darwin'" }, + { name = "propcache", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From f1eebdbb043bf6fd41c47ec3f4256a14f62720da Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 7 Aug 2025 16:34:08 +0200 Subject: [PATCH 037/113] Updated code examples. --- 2025/aidesign/chain_of_responsibility.py | 50 +++++-- 2025/aidesign/observer.py | 179 +++++++++++------------ 2 files changed, 120 insertions(+), 109 deletions(-) diff --git a/2025/aidesign/chain_of_responsibility.py b/2025/aidesign/chain_of_responsibility.py index b68ba29e..01573ed2 100644 --- a/2025/aidesign/chain_of_responsibility.py +++ b/2025/aidesign/chain_of_responsibility.py @@ -19,6 +19,7 @@ @dataclass class TravelDeps: user_name: str + origin_city: str # ---------------------------------- @@ -112,47 +113,64 @@ class Activities(BaseModel): # Chain Execution Logic # ---------------------------------- - -async def plan_trip(user_input: str, deps: TravelDeps): - print(f"\n๐Ÿ‘ค {deps.user_name} says: {user_input}") - ctx = TripContext() - - # Step 1: Destination - dest_result = await destination_agent.run(prompt=user_input, deps=deps) +async def handle_destination( + user_input: str, deps: TravelDeps, ctx: TripContext +) -> None: + dest_result = await destination_agent.run(user_input, deps=deps) ctx.destination = dest_result.output.destination print(f"๐Ÿ“ Destination: {ctx.destination}") - # Step 2: Flight - flight_prompt = f"Plan a flight to {ctx.destination}." - flight_result = await flight_agent.run(prompt=flight_prompt, deps=deps) +async def handle_flight( + user_input: str, deps: TravelDeps, ctx: TripContext +) -> None: + flight_prompt = f"Plan a flight from {deps.origin_city} to {ctx.destination}." + flight_result = await flight_agent.run(flight_prompt, deps=deps) ctx.from_city = flight_result.output.from_city ctx.arrival_time = flight_result.output.arrival_time print( f"โœˆ๏ธ Flight: from {ctx.from_city} โ†’ {ctx.destination}, arriving at {ctx.arrival_time}" ) - # Step 3: Hotel +async def handle_hotel( + user_input: str, deps: TravelDeps, ctx: TripContext +) -> None: hotel_prompt = ( f"Recommend a hotel in {ctx.destination} for a traveler arriving at {ctx.arrival_time}. " f"Prefer locations near the airport or city center." ) - hotel_result = await hotel_agent.run(prompt=hotel_prompt, deps=deps) + hotel_result = await hotel_agent.run(hotel_prompt, deps=deps) ctx.hotel_name = hotel_result.output.name ctx.hotel_location = hotel_result.output.location print( f"๐Ÿจ Hotel: {ctx.hotel_name}, {hotel_result.output.stars}โ˜… at ${hotel_result.output.price_per_night_usd}/night" ) - # Step 4: Activities +async def handle_activities( + user_input: str, deps: TravelDeps, ctx: TripContext +) -> None: activities_prompt = ( f"Suggest activities in {ctx.destination} close to {ctx.hotel_location} " f"and suitable for a traveler arriving at {ctx.arrival_time}." ) - activity_result = await activity_agent.run(prompt=activities_prompt, deps=deps) + activity_result = await activity_agent.run(activities_prompt, deps=deps) print(f"๐ŸŽฏ Activities for {activity_result.output.personalized_for}:") for a in activity_result.output.top_activities: print(f" - {a}") +async def plan_trip(user_input: str, deps: TravelDeps): + print(f"\n๐Ÿ‘ค {deps.user_name} says: {user_input}") + ctx = TripContext() + + chain = [ + handle_destination, + handle_flight, + handle_hotel, + handle_activities, + ] + + for step in chain: + await step(user_input, deps, ctx) + # ---------------------------------- # Main Execution @@ -160,8 +178,8 @@ async def plan_trip(user_input: str, deps: TravelDeps): async def main(): - deps = TravelDeps(user_name="Maria") - await plan_trip("I want a quiet, sunny destination near the ocean.", deps) + deps = TravelDeps(user_name="Maria", origin_city="Berlin") + await plan_trip("I want a rainy city trip within Europe.", deps) if __name__ == "__main__": diff --git a/2025/aidesign/observer.py b/2025/aidesign/observer.py index 590ead04..a3e5c3d0 100644 --- a/2025/aidesign/observer.py +++ b/2025/aidesign/observer.py @@ -1,131 +1,124 @@ import asyncio import os +import time from dataclasses import dataclass +from typing import Protocol from dotenv import load_dotenv -from pydantic import BaseModel, Field +from pydantic import BaseModel from pydantic_ai import Agent -# Load API key load_dotenv() os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") -# ------------------------------- -# Dependencies -# ------------------------------- +# ---------------------------------- +# Dependencies and Output Schema +# ---------------------------------- @dataclass class TravelDeps: user_name: str - - -# ------------------------------- -# Main Response Output -# ------------------------------- + origin_city: str class TravelResponse(BaseModel): - message: str = Field(..., description="Response to the user") - destination: str = Field(..., description="Suggested destination") + destination: str + message: str -# ------------------------------- -# Structured Log Entry -# ------------------------------- +# ---------------------------------- +# Observer Interface +# ---------------------------------- + +class AgentCallObserver(Protocol): + def notify( + self, + agent_name: str, + prompt: str, + deps: TravelDeps, + output: BaseModel, + duration: float, + ) -> None: + ... + + +class ConsoleLogger(AgentCallObserver): + def notify( + self, + agent_name: str, + prompt: str, + deps: TravelDeps, + output: BaseModel, + duration: float, + ) -> None: + print("\n๐Ÿ“‹ Agent Call Log") + print(f"Agent: {agent_name}") + print(f"Prompt: {prompt}") + print(f"User: {deps.user_name}, Origin: {deps.origin_city}") + print(f"Output: {output.model_dump()}") + print(f"Duration: {duration:.2f}s") + + +# ---------------------------------- +# Wrapper to run agent with observers +# ---------------------------------- + +async def run_with_observers( + *, + agent: Agent[TravelDeps, BaseModel], + prompt: str, + deps: TravelDeps, + observers: list[AgentCallObserver], +) -> TravelResponse: + start = time.perf_counter() + result = await agent.run(prompt, deps=deps) + end = time.perf_counter() + duration = end - start + for observer in observers: + observer.notify( + agent_name=agent.name or "Unnamed Agent", + prompt=prompt, + deps=deps, + output=result.output, + duration=duration, + ) -class LogEntry(BaseModel): - level: str = Field(..., description="Log level: info, warning, error, etc.") - message: str = Field(..., description="What the agent did") - source: str = Field( - ..., description="The part of the system that generated the log" - ) + return result.output -# ------------------------------- -# Agent: Travel Recommender -# ------------------------------- +# ---------------------------------- +# Agent Definition +# ---------------------------------- travel_agent = Agent( "openai:gpt-4o", + name="TravelAgent", deps_type=TravelDeps, output_type=TravelResponse, - system_prompt="You are a helpful travel assistant. Recommend a good destination and respond politely.", + system_prompt="You are a friendly travel assistant. Recommend a destination based on user preferences.", ) -# ------------------------------- -# Log Agent: Parallel Output -# ------------------------------- - -log_agent = Agent( - "openai:gpt-4o", - deps_type=TravelDeps, - output_type=LogEntry, - system_prompt="Log what the travel agent just did in a structured format.", -) - - -# ------------------------------- -# Observer Interface -# ------------------------------- - - -class Observer: - def notify(self, log: LogEntry): - pass - +# ---------------------------------- +# Main Program +# ---------------------------------- -# ------------------------------- -# Example Concrete Observer -# ------------------------------- - - -class ConsoleLogger(Observer): - def notify(self, log: LogEntry): - print(f"[{log.level.upper()}] from {log.source}: {log.message}") - - -# ------------------------------- -# Execution Function -# ------------------------------- - - -async def recommend_travel( - user_prompt: str, deps: TravelDeps, observers: list[Observer] -): - # Step 1: Get the response from the agent - response_result = await travel_agent.run(user_prompt, deps=deps) - response = response_result.output - - # Step 2: Generate structured log from a separate agent - log_prompt = ( - f"The agent suggested {response.destination} in response to a user prompt." +async def main(): + deps = TravelDeps(user_name="Nina", origin_city="Copenhagen") + + prompt = "I want to escape to a cozy place in the mountains for the weekend." + output = await run_with_observers( + agent=travel_agent, + prompt=prompt, + deps=deps, + observers=[ConsoleLogger()], ) - log_result = await log_agent.run(log_prompt, deps=deps) - log_entry = log_result.output - # Step 3: Notify observers - for observer in observers: - observer.notify(log_entry) - - # Step 4: Show user response - print(f"\n๐Ÿ‘ค {deps.user_name} asked: {user_prompt}") - print(f"๐Ÿค– Travel Agent says: {response.message}") - print(f"๐Ÿ“ Destination Suggested: {response.destination}") - - -# ------------------------------- -# Main -# ------------------------------- - - -async def main(): - deps = TravelDeps(user_name="Alex") - observers = [ConsoleLogger()] - await recommend_travel("I want to go somewhere warm with beaches.", deps, observers) + print(f"\n๐Ÿค– Travel Agent says: {output.message}") + print(f"๐Ÿ“ Destination Suggested: {output.destination}") if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file From 4d08d8d426237157467526b938afa67334964dd5 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 12 Aug 2025 13:09:31 +0200 Subject: [PATCH 038/113] Removed unneccesary environment variable setting. --- 2025/aidesign/chain_of_responsibility.py | 2 -- 2025/aidesign/observer.py | 2 -- 2025/aidesign/strategy.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/2025/aidesign/chain_of_responsibility.py b/2025/aidesign/chain_of_responsibility.py index 01573ed2..62381081 100644 --- a/2025/aidesign/chain_of_responsibility.py +++ b/2025/aidesign/chain_of_responsibility.py @@ -1,5 +1,4 @@ import asyncio -import os from dataclasses import dataclass from typing import Optional @@ -9,7 +8,6 @@ # Load env vars load_dotenv() -os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") # ---------------------------------- # Shared Dependencies diff --git a/2025/aidesign/observer.py b/2025/aidesign/observer.py index a3e5c3d0..265f21d8 100644 --- a/2025/aidesign/observer.py +++ b/2025/aidesign/observer.py @@ -1,5 +1,4 @@ import asyncio -import os import time from dataclasses import dataclass from typing import Protocol @@ -9,7 +8,6 @@ from pydantic_ai import Agent load_dotenv() -os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") # ---------------------------------- diff --git a/2025/aidesign/strategy.py b/2025/aidesign/strategy.py index 59f73c7b..d8dd899a 100644 --- a/2025/aidesign/strategy.py +++ b/2025/aidesign/strategy.py @@ -1,5 +1,4 @@ import asyncio -import os from dataclasses import dataclass from typing import Callable @@ -9,7 +8,6 @@ # Load API key load_dotenv() -os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "") # ---------------------------------- # Common Output Model From 7262f3215fb5014fa8df4b2e7de942c31978bf4e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 7 Aug 2025 14:52:03 +0200 Subject: [PATCH 039/113] Added singleton code examples. --- 2025/singleton/basic_example.py | 23 +++++++++++++++++++++ 2025/singleton/config.py | 2 ++ 2025/singleton/instance_control.py | 29 +++++++++++++++++++++++++++ 2025/singleton/metaclass.py | 27 +++++++++++++++++++++++++ 2025/singleton/module_example.py | 8 ++++++++ 2025/singleton/singleton_multi.py | 27 +++++++++++++++++++++++++ 2025/singleton/singleton_threading.py | 22 ++++++++++++++++++++ 7 files changed, 138 insertions(+) create mode 100644 2025/singleton/basic_example.py create mode 100644 2025/singleton/config.py create mode 100644 2025/singleton/instance_control.py create mode 100644 2025/singleton/metaclass.py create mode 100644 2025/singleton/module_example.py create mode 100644 2025/singleton/singleton_multi.py create mode 100644 2025/singleton/singleton_threading.py diff --git a/2025/singleton/basic_example.py b/2025/singleton/basic_example.py new file mode 100644 index 00000000..36666e04 --- /dev/null +++ b/2025/singleton/basic_example.py @@ -0,0 +1,23 @@ +class Config: + _instance = None + + def __init__(self): + self.db_url = "sqlite:///:memory:" + self.debug = True + + def __new__(cls): + if cls._instance is None: + print("Creating new instance") + cls._instance = super().__new__(cls) + return cls._instance + +def main(): + s1 = Config() + s2 = Config() + + print(s1 is s2) + print(id(s1)) + print(id(s2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/singleton/config.py b/2025/singleton/config.py new file mode 100644 index 00000000..215afabc --- /dev/null +++ b/2025/singleton/config.py @@ -0,0 +1,2 @@ +db_uri = "sqlite://:memory:" +debug = True \ No newline at end of file diff --git a/2025/singleton/instance_control.py b/2025/singleton/instance_control.py new file mode 100644 index 00000000..9306188d --- /dev/null +++ b/2025/singleton/instance_control.py @@ -0,0 +1,29 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class ModelLoader(metaclass=Singleton): + def __init__(self): + print("Loading large model...") + + def predict(self, data: str) -> str: + return f"Prediction for {data}" + + +def predict(data: str) -> str: + model = ModelLoader() + return model.predict(data) + +def main() -> None: + predictions = [predict(f"data_{i}") for i in range(5)] + for pred in predictions: + print(pred) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/singleton/metaclass.py b/2025/singleton/metaclass.py new file mode 100644 index 00000000..b0237814 --- /dev/null +++ b/2025/singleton/metaclass.py @@ -0,0 +1,27 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + +class Config(metaclass=Singleton): + def __init__(self): + self.db_url = "sqlite:///:memory:" + self.debug = True + + def __str__(self) -> str: + return f"Config(db_url={self.db_url}, debug={self.debug})" + +def main() -> None: + s1 = Config() + s2 = Config() + + print(s1 is s2) # Should print True, indicating both are the same instance + print(id(s1)) # Prints the instance id + print(id(s2)) # Prints the same instance id as s1 + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/singleton/module_example.py b/2025/singleton/module_example.py new file mode 100644 index 00000000..e9c3df18 --- /dev/null +++ b/2025/singleton/module_example.py @@ -0,0 +1,8 @@ +import config + +def main() -> None: + if config.debug: + print(config.db_uri) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/singleton/singleton_multi.py b/2025/singleton/singleton_multi.py new file mode 100644 index 00000000..ec35a759 --- /dev/null +++ b/2025/singleton/singleton_multi.py @@ -0,0 +1,27 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + +class Config(metaclass=Singleton): + def __init__(self): + self.db_url = "sqlite:///:memory:" + self.debug = True + + def __str__(self) -> str: + return f"Config(db_url={self.db_url}, debug={self.debug})" + +def main() -> None: + c1 = Config() + c2 = Config.__new__(Config) + c2.__init__() + + print(c1 is c2) # โŒ False + +if __name__ == "__main__": + main() + diff --git a/2025/singleton/singleton_threading.py b/2025/singleton/singleton_threading.py new file mode 100644 index 00000000..e1b6d2bd --- /dev/null +++ b/2025/singleton/singleton_threading.py @@ -0,0 +1,22 @@ +from threading import Thread + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + +class Unsafe(metaclass=Singleton): + def __init__(self): + print("Initializing...") + +def main() -> None: + threads = [Thread(target=Unsafe) for _ in range(20)] + [t.start() for t in threads] + [t.join() for t in threads] + +if __name__ == "__main__": + main() \ No newline at end of file From 49ac78038f9bfab6c1bbe2b2a2f43dfc2a853892 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 12 Aug 2025 14:07:36 +0200 Subject: [PATCH 040/113] Added pyproject file. Minor cleanup. Added thread-safe example. --- 2025/singleton/instance_control.py | 13 +++------- 2025/singleton/metaclass.py | 14 ++++------- 2025/singleton/pyproject.toml | 7 ++++++ 2025/singleton/singleton.py | 8 ++++++ 2025/singleton/singleton_multi.py | 13 +++------- 2025/singleton/singleton_safe.py | 29 ++++++++++++++++++++++ 2025/singleton/singleton_threading.py | 14 ++++------- 2025/singleton/singleton_threading_safe.py | 18 ++++++++++++++ 2025/singleton/uv.lock | 8 ++++++ 9 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 2025/singleton/pyproject.toml create mode 100644 2025/singleton/singleton.py create mode 100644 2025/singleton/singleton_safe.py create mode 100644 2025/singleton/singleton_threading_safe.py create mode 100644 2025/singleton/uv.lock diff --git a/2025/singleton/instance_control.py b/2025/singleton/instance_control.py index 9306188d..9320c3fe 100644 --- a/2025/singleton/instance_control.py +++ b/2025/singleton/instance_control.py @@ -1,11 +1,4 @@ -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - print(f"Creating instance of {cls.__name__}") - cls._instances[cls] = super().__call__(*args, **kwargs) - return cls._instances[cls] +from singleton import Singleton class ModelLoader(metaclass=Singleton): @@ -20,10 +13,12 @@ def predict(data: str) -> str: model = ModelLoader() return model.predict(data) + def main() -> None: predictions = [predict(f"data_{i}") for i in range(5)] for pred in predictions: print(pred) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/singleton/metaclass.py b/2025/singleton/metaclass.py index b0237814..d1eef02f 100644 --- a/2025/singleton/metaclass.py +++ b/2025/singleton/metaclass.py @@ -1,11 +1,5 @@ -class Singleton(type): - _instances = {} +from singleton import Singleton - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - print(f"Creating instance of {cls.__name__}") - cls._instances[cls] = super().__call__(*args, **kwargs) - return cls._instances[cls] class Config(metaclass=Singleton): def __init__(self): @@ -14,7 +8,8 @@ def __init__(self): def __str__(self) -> str: return f"Config(db_url={self.db_url}, debug={self.debug})" - + + def main() -> None: s1 = Config() s2 = Config() @@ -23,5 +18,6 @@ def main() -> None: print(id(s1)) # Prints the instance id print(id(s2)) # Prints the same instance id as s1 + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/singleton/pyproject.toml b/2025/singleton/pyproject.toml new file mode 100644 index 00000000..7f778fca --- /dev/null +++ b/2025/singleton/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "singleton" +version = "0.1.0" +description = "Singleton code example" +requires-python = ">=3.13" +dependencies = [ +] diff --git a/2025/singleton/singleton.py b/2025/singleton/singleton.py new file mode 100644 index 00000000..7fa69706 --- /dev/null +++ b/2025/singleton/singleton.py @@ -0,0 +1,8 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/2025/singleton/singleton_multi.py b/2025/singleton/singleton_multi.py index ec35a759..03a16faf 100644 --- a/2025/singleton/singleton_multi.py +++ b/2025/singleton/singleton_multi.py @@ -1,11 +1,5 @@ -class Singleton(type): - _instances = {} +from singleton import Singleton - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - print(f"Creating instance of {cls.__name__}") - cls._instances[cls] = super().__call__(*args, **kwargs) - return cls._instances[cls] class Config(metaclass=Singleton): def __init__(self): @@ -14,7 +8,8 @@ def __init__(self): def __str__(self) -> str: return f"Config(db_url={self.db_url}, debug={self.debug})" - + + def main() -> None: c1 = Config() c2 = Config.__new__(Config) @@ -22,6 +17,6 @@ def main() -> None: print(c1 is c2) # โŒ False + if __name__ == "__main__": main() - diff --git a/2025/singleton/singleton_safe.py b/2025/singleton/singleton_safe.py new file mode 100644 index 00000000..3b955f62 --- /dev/null +++ b/2025/singleton/singleton_safe.py @@ -0,0 +1,29 @@ +# singleton.py +from __future__ import annotations + +import threading +from typing import Any + + +class Singleton(type): + _instances: dict[type, Any] = {} + _locks: dict[type, threading.Lock] = {} + _global_lock = threading.Lock() # protects _locks map creation + + def __call__(cls, *args, **kwargs): + # Fast path: already created + if cls in cls._instances: + return cls._instances[cls] + + # Ensure a per-class lock exists + with cls._global_lock: + lock = cls._locks.setdefault(cls, threading.Lock()) + + # Double-checked locking: only one thread creates the instance + with lock: + if cls not in cls._instances: + print(f"Creating instance of {cls.__name__}") + instance = super().__call__(*args, **kwargs) # calls __init__ once + cls._instances[cls] = instance + + return cls._instances[cls] diff --git a/2025/singleton/singleton_threading.py b/2025/singleton/singleton_threading.py index e1b6d2bd..d3518c14 100644 --- a/2025/singleton/singleton_threading.py +++ b/2025/singleton/singleton_threading.py @@ -1,22 +1,18 @@ from threading import Thread -class Singleton(type): - _instances = {} +from singleton import Singleton + - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - print(f"Creating instance of {cls.__name__}") - cls._instances[cls] = super().__call__(*args, **kwargs) - return cls._instances[cls] - class Unsafe(metaclass=Singleton): def __init__(self): print("Initializing...") + def main() -> None: threads = [Thread(target=Unsafe) for _ in range(20)] [t.start() for t in threads] [t.join() for t in threads] + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/singleton/singleton_threading_safe.py b/2025/singleton/singleton_threading_safe.py new file mode 100644 index 00000000..4dd453e4 --- /dev/null +++ b/2025/singleton/singleton_threading_safe.py @@ -0,0 +1,18 @@ +from threading import Thread + +from singleton_safe import Singleton + + +class Unsafe(metaclass=Singleton): + def __init__(self): + print("Initializing...") + + +def main() -> None: + threads = [Thread(target=Unsafe) for _ in range(20)] + [t.start() for t in threads] + [t.join() for t in threads] + + +if __name__ == "__main__": + main() diff --git a/2025/singleton/uv.lock b/2025/singleton/uv.lock new file mode 100644 index 00000000..e7a4c07b --- /dev/null +++ b/2025/singleton/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "singleton" +version = "0.1.0" +source = { virtual = "." } From a6d1c0d62dc859a7a4603f23506388c3ac39ecf7 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 30 Jul 2025 17:12:08 +0200 Subject: [PATCH 041/113] Added code examples for SOLID video. --- 2025/solid/class_based_report.py | 121 ++++++++++++++++++++++++++++ 2025/solid/functional_report.py | 91 +++++++++++++++++++++ 2025/solid/messy_report.py | 54 +++++++++++++ 2025/solid/pyproject.toml | 8 ++ 2025/solid/sales_data.csv | 51 ++++++++++++ 2025/solid/uv.lock | 132 +++++++++++++++++++++++++++++++ 6 files changed, 457 insertions(+) create mode 100644 2025/solid/class_based_report.py create mode 100644 2025/solid/functional_report.py create mode 100644 2025/solid/messy_report.py create mode 100644 2025/solid/pyproject.toml create mode 100644 2025/solid/sales_data.csv create mode 100644 2025/solid/uv.lock diff --git a/2025/solid/class_based_report.py b/2025/solid/class_based_report.py new file mode 100644 index 00000000..86ff6054 --- /dev/null +++ b/2025/solid/class_based_report.py @@ -0,0 +1,121 @@ +import json +from dataclasses import dataclass +from datetime import datetime +from typing import Protocol + +import pandas as pd + + +@dataclass +class ReportConfig: + input_file: str + output_file: str + start_date: datetime | None = None + end_date: datetime | None = None + + +class SalesReader(Protocol): + def read(self, file: str) -> pd.DataFrame: ... + + +class CsvSalesReader: + def read(self, file: str) -> pd.DataFrame: + return pd.read_csv(file, parse_dates=["date"]) + + +class DateRangeFilter: + def apply( + self, df: pd.DataFrame, start: datetime | None, end: datetime | None + ) -> pd.DataFrame: + if start: + df = df[df["date"] >= pd.Timestamp(start)] + if end: + df = df[df["date"] <= pd.Timestamp(end)] + return df + + +class Metric(Protocol): + def compute(self, df: pd.DataFrame) -> dict[str, object]: ... + + +class CustomerCountMetric: + def compute(self, df: pd.DataFrame) -> dict[str, object]: + return {"number_of_customers": df["name"].nunique()} + + +class AverageOrderValueMetric: + def compute(self, df: pd.DataFrame) -> dict[str, object]: + sales = df[df["price"] > 0]["price"] + avg = sales.mean() if not sales.empty else 0.0 + return {"average_order_value (pre-tax)": round(avg, 2)} + + +class ReturnPercentageMetric: + def compute(self, df: pd.DataFrame) -> dict[str, object]: + returns = df[df["price"] < 0] + pct = (len(returns) / len(df)) * 100 if len(df) > 0 else 0 + return {"percentage_of_returns": round(pct, 2)} + + +class TotalSalesMetric: + def compute(self, df: pd.DataFrame) -> dict[str, object]: + return {"total_sales_in_period (pre-tax)": round(df["price"].sum(), 2)} + + +class SalesReportGenerator: + def __init__( + self, reader: SalesReader, filterer: DateRangeFilter, metrics: list[Metric] + ): + self.reader = reader + self.filterer = filterer + self.metrics = metrics + + def generate(self, config: ReportConfig) -> dict[str, object]: + df = self.reader.read(config.input_file) + df = self.filterer.apply(df, config.start_date, config.end_date) + + result = {} + for metric in self.metrics: + result.update(metric.compute(df)) + + result["report_start"] = ( + config.start_date.strftime("%Y-%m-%d") if config.start_date else "N/A" + ) + result["report_end"] = ( + config.end_date.strftime("%Y-%m-%d") if config.end_date else "N/A" + ) + return result + + +class JSONReportWriter: + def write(self, report: dict[str, object], output_file: str) -> None: + with open(output_file, "w") as f: + json.dump(report, f, indent=2) + + +def main() -> None: + config = ReportConfig( + input_file="sales_data.csv", + output_file="sales_report.json", + start_date=datetime(2024, 1, 1), + end_date=datetime(2024, 12, 31), + ) + + reader = CsvSalesReader() + filterer = DateRangeFilter() + metrics: list[Metric] = [ + CustomerCountMetric(), + AverageOrderValueMetric(), + ReturnPercentageMetric(), + TotalSalesMetric(), + ] + + generator = SalesReportGenerator(reader, filterer, metrics) + report = generator.generate(config) + + writer = JSONReportWriter() + writer.write(report, config.output_file) + + +if __name__ == "__main__": + main() diff --git a/2025/solid/functional_report.py b/2025/solid/functional_report.py new file mode 100644 index 00000000..3ab6e49a --- /dev/null +++ b/2025/solid/functional_report.py @@ -0,0 +1,91 @@ +import json +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Callable + +import pandas as pd + +type MetricFn = Callable[[pd.DataFrame], dict[str, Any]] + + +@dataclass +class ReportConfig: + input_file: str + start_date: datetime | None = None + end_date: datetime | None = None + metrics: list[MetricFn] = field(default_factory=list) + + +def read_sales(file: str) -> pd.DataFrame: + return pd.read_csv(file, parse_dates=["date"]) + + +def filter_sales( + df: pd.DataFrame, start: datetime | None, end: datetime | None +) -> pd.DataFrame: + if start: + df = df[df["date"] >= pd.Timestamp(start)] + if end: + df = df[df["date"] <= pd.Timestamp(end)] + return df + + +def customer_count_metric(df: pd.DataFrame) -> dict[str, Any]: + return {"number_of_customers": df["name"].nunique()} + + +def average_order_value_metric(df: pd.DataFrame) -> dict[str, Any]: + sales = df[df["price"] > 0]["price"] + avg = sales.mean() if not sales.empty else 0.0 + return {"average_order_value (pre-tax)": round(avg, 2)} + + +def return_percentage_metric(df: pd.DataFrame) -> dict[str, Any]: + returns = df[df["price"] < 0] + pct = (len(returns) / len(df)) * 100 if len(df) > 0 else 0 + return {"percentage_of_returns": round(pct, 2)} + + +def total_sales_metric(df: pd.DataFrame) -> dict[str, Any]: + return {"total_sales_in_period (pre-tax)": round(df["price"].sum(), 2)} + + +def generate_report_data(df: pd.DataFrame, config: ReportConfig) -> dict[str, Any]: + result: dict[str, Any] = {} + for metric in config.metrics: + result.update(metric(df)) + result["report_start"] = ( + config.start_date.strftime("%Y-%m-%d") if config.start_date else "N/A" + ) + result["report_end"] = ( + config.end_date.strftime("%Y-%m-%d") if config.end_date else "N/A" + ) + return result + + +def write_report(data: dict[str, Any], filename: str): + with open(filename, "w") as f: + json.dump(data, f, indent=2) + + +def main() -> None: + config = ReportConfig( + input_file="sales_data.csv", + start_date=datetime(2024, 1, 1), + end_date=datetime(2024, 12, 31), + metrics=[ + customer_count_metric, + average_order_value_metric, + return_percentage_metric, + total_sales_metric, + ], + ) + + df = read_sales(config.input_file) + df = filter_sales(df, config.start_date, config.end_date) + report_data = generate_report_data(df, config) + write_report(report_data, "sales_report.json") + + +if __name__ == "__main__": + main() diff --git a/2025/solid/messy_report.py b/2025/solid/messy_report.py new file mode 100644 index 00000000..85b8ca68 --- /dev/null +++ b/2025/solid/messy_report.py @@ -0,0 +1,54 @@ +import json +from datetime import datetime + +import pandas as pd + + +class MessySalesReport: + def generate( + self, + input_file: str, + output_file: str, + start_date: datetime | None = None, + end_date: datetime | None = None, + ) -> None: + df = pd.read_csv(input_file, parse_dates=["date"]) + + if start_date: + df = df[df["date"] >= pd.Timestamp(start_date)] + if end_date: + df = df[df["date"] <= pd.Timestamp(end_date)] + + num_customers = df["name"].nunique() + avg_order = ( + df[df["price"] > 0]["price"].mean() if not df[df["price"] > 0].empty else 0 + ) + returns = df[df["price"] < 0] + return_pct = (len(returns) / len(df)) * 100 if len(df) > 0 else 0 + total_sales = df["price"].sum() + + report = { + "report_start": start_date.strftime("%Y-%m-%d") if start_date else "N/A", + "report_end": end_date.strftime("%Y-%m-%d") if end_date else "N/A", + "number_of_customers": num_customers, + "average_order_value (pre-tax)": round(avg_order, 2), + "percentage_of_returns": round(return_pct, 2), + "total_sales_in_period (pre-tax)": round(total_sales, 2), + } + + with open(output_file, "w") as f: + json.dump(report, f, indent=2) + + +def main() -> None: + report = MessySalesReport() + report.generate( + input_file="sales_data.csv", + output_file="sales_report.json", + start_date=datetime(2024, 1, 1), + end_date=datetime(2024, 12, 31), + ) + + +if __name__ == "__main__": + main() diff --git a/2025/solid/pyproject.toml b/2025/solid/pyproject.toml new file mode 100644 index 00000000..9f9cf8a3 --- /dev/null +++ b/2025/solid/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "solid" +version = "0.1.0" +description = "Code refactoring using the SOLID principles" +requires-python = ">=3.13" +dependencies = [ + "pandas>=2.3.1", +] diff --git a/2025/solid/sales_data.csv b/2025/solid/sales_data.csv new file mode 100644 index 00000000..8627b4fe --- /dev/null +++ b/2025/solid/sales_data.csv @@ -0,0 +1,51 @@ +"name","address","item","date","price","tax" +"John Guerrero","1344 Reed Spring Suite 944, Port Jonathan, DC 08436","Keyboard","2024-05-07",463.8,97.4 +"Molly Cowan","3783 Kelly Parks, Hoffmanville, MS 81569","Keyboard","2024-06-21",267.63,56.2 +"Lori Porter","318 Travis Trace, North Matthew, LA 12381","Desk","2023-05-11",986.89,207.25 +"Philip Miller","8018 Lee Drive Suite 871, Gutierreztown, NV 03929","Keyboard","2024-01-29",172.52,36.23 +"David Ponce","89433 Pruitt Plaza, Christensenland, KS 04867","Monitor","2024-04-14",-536.66,-112.7 +"Diane Greene","339 James Lake Suite 899, Bennettshire, ND 61981","Keyboard","2024-05-28",1169.86,245.67 +"Kim Brock","699 Mcdaniel Mountain Suite 815, West Julie, AK 78647","Monitor","2024-06-22",155.89,32.74 +"Jennifer Sellers","0630 Burke Summit, Port William, PA 16545","Keyboard","2023-09-04",633.84,133.11 +"Melinda Dennis","6019 Brian Trail, Lake Michellestad, WY 89381","Desk","2023-08-18",520.8,109.37 +"Miss Joanna Contreras DVM","217 Moon Wells Suite 288, Ortizfort, NE 87937","Chair","2023-09-07",1086.12,228.09 +"Angela Chapman","Unit 0607 Box 4655, DPO AP 12818","Laptop","2023-10-02",257.75,54.13 +"Jeffrey Tucker","737 Graves Wells Suite 676, Lake Hailey, RI 98451","Headphones","2024-09-04",519.26,109.04 +"Scott Rasmussen","3265 Delacruz Mountain, Hoffmanborough, TX 46752","Monitor","2023-08-21",421.92,88.6 +"Larry Erickson","16412 Smith Garden, Edwardstad, NV 82892","Mouse","2023-07-03",477.85,100.35 +"Rebecca Buchanan","9418 April Flat Suite 112, Jasonfurt, CA 80823","Desk","2023-03-17",1302.67,273.56 +"Mary Hill","8691 Clark Avenue, South Jessicachester, ME 26317","Chair","2024-09-18",935.48,196.45 +"Robin Oliver","1120 Matthew Rest, Tuckershire, CA 98947","Chair","2024-04-20",-428.78,-90.04 +"Charles Scott","27510 Aaron Inlet Suite 851, East Kelseychester, NH 55606","Chair","2023-10-31",1102.14,231.45 +"Sandy Spencer","19166 Blair Lane, Kellifurt, MI 37349","Keyboard","2024-09-18",84.55,17.76 +"Eric Dunn","55700 Rivera Loaf Apt. 771, West Chelsea, TX 23511","Mouse","2024-11-27",1257.19,264.01 +"Dr. Charles Myers","71166 Anthony Crossroad, Donnaport, NV 79264","Mouse","2023-07-21",704.39,147.92 +"Amanda Wallace","2237 Brandon Turnpike Suite 629, Loweberg, TN 07617","Chair","2024-09-25",1235.35,259.42 +"Michael Spencer","01698 Cox Freeway Apt. 043, East Douglas, NE 68236","Keyboard","2023-01-12",732.29,153.78 +"Sylvia Mitchell","716 Gregory Locks Apt. 086, Lake Chelsea, AL 85891","Laptop","2024-06-08",-78.58,-16.5 +"Gregory Hansen","718 Veronica Mountains Apt. 232, East John, NY 17047","Headphones","2024-02-18",878.4,184.46 +"Brenda Jackson","588 Donald Estate, East Michellefort, ND 75420","Keyboard","2024-08-25",1053.68,221.27 +"Alexander Lewis","61716 Deborah Coves, Mcdanielfurt, CA 04531","Laptop","2024-12-18",158.75,33.34 +"Dawn Kane","USNS Anderson, FPO AP 76056","Monitor","2023-11-03",786.68,165.2 +"Robert Roberts","6256 Padilla Center, Lake Shawnshire, RI 34060","Keyboard","2024-07-25",955.43,200.64 +"Steven Stanton","0208 Johnston Corner, South Sheila, NH 10538","Mouse","2024-06-19",1158.38,243.26 +"Adam English","296 Isaiah Meadow, Hunthaven, NE 01384","Laptop","2024-10-08",1013.07,212.74 +"Angela Lara","799 Susan Extension Apt. 955, Susantown, AR 95518","Mouse","2024-10-03",889.66,186.83 +"Joshua Townsend","209 Munoz Brook Suite 145, Ericmouth, TN 24982","Keyboard","2024-07-21",1249.22,262.34 +"Rhonda Johnson","PSC 1781, Box 0242, APO AE 36561","Headphones","2024-07-15",1051.27,220.77 +"Rachel Shaw","4463 Romero Keys, West Leslieville, GA 64799","Chair","2023-12-29",-317.42,-66.66 +"Mackenzie Blevins","PSC 2008, Box 5610, APO AP 24541","Desk","2024-07-04",453.37,95.21 +"Jaime Brown","924 Monica Key Apt. 814, Margaretberg, NC 39764","Monitor","2024-05-09",-568.46,-119.38 +"Ronald Williams II","1629 Brittany Turnpike Apt. 725, East Terry, ME 94547","Desk","2024-12-02",534.26,112.19 +"Joseph White","3564 Anderson Canyon, Perezmouth, NC 35482","Chair","2024-11-01",1454.68,305.48 +"Marie Brown","04699 Kimberly Rapid, Smithborough, WY 73900","Chair","2024-08-08",393.65,82.67 +"Stephanie Brown MD","06243 Short Cape, Johnbury, DE 54999","Mouse","2024-08-01",949.32,199.36 +"Luke Mccoy","7186 Newton Trail Apt. 182, Emilyton, DE 42065","Headphones","2023-04-28",764.15,160.47 +"Ralph Gonzalez","6396 Todd Squares, Georgechester, UT 14672","Headphones","2023-04-05",822.79,172.79 +"Michele Medina","716 Melissa Turnpike, Forbesfurt, MI 98049","Headphones","2024-06-10",647.54,135.98 +"Joseph Reed","Unit 4989 Box 6489, DPO AP 24839","Desk","2024-08-15",626.1,131.48 +"Eric Parker","7648 Robert Estates Apt. 485, Stevensland, AZ 29898","Mouse","2024-01-22",1236.03,259.57 +"Ashley Howe","837 Debra Ramp Suite 872, Port Tiffany, LA 39492","Monitor","2024-11-07",1321.36,277.49 +"Sean Briggs","791 Jose Knolls, Harrischester, ND 41641","Mouse","2023-09-18",1409.34,295.96 +"Donna Woods","92886 Crawford Point Apt. 749, Wellsfurt, CO 18301","Mouse","2024-12-16",721.4,151.49 +"Sara Sandoval","Unit 9935 Box 3484, DPO AA 53240","Keyboard","2023-08-12",1246.37,261.74 diff --git a/2025/solid/uv.lock b/2025/solid/uv.lock new file mode 100644 index 00000000..37b43f45 --- /dev/null +++ b/2025/solid/uv.lock @@ -0,0 +1,132 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "solid" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [{ name = "pandas", specifier = ">=2.3.1" }] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] From b32d88a7b85916f36d153eb0703d6bc12fd8a439 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 12 Aug 2025 10:57:10 +0200 Subject: [PATCH 042/113] Added project code example. --- 2025/project/.env.example | 2 + 2025/project/.python-version | 1 + 2025/project/Dockerfile | 24 ++ 2025/project/README.md | 39 +++ 2025/project/app/api/v1/user.py | 47 +++ 2025/project/app/core/config.py | 19 ++ 2025/project/app/core/logging.py | 7 + 2025/project/app/db/schema.py | 18 ++ 2025/project/app/main.py | 15 + 2025/project/app/models/user.py | 10 + 2025/project/app/services/user_service.py | 38 +++ 2025/project/docker-compose.yaml | 9 + 2025/project/pyproject.toml | 18 ++ 2025/project/tests/api/v1/test_user.py | 34 ++ 2025/project/tests/test_db.py | 16 + 2025/project/uv.lock | 369 ++++++++++++++++++++++ 16 files changed, 666 insertions(+) create mode 100644 2025/project/.env.example create mode 100644 2025/project/.python-version create mode 100644 2025/project/Dockerfile create mode 100644 2025/project/README.md create mode 100644 2025/project/app/api/v1/user.py create mode 100644 2025/project/app/core/config.py create mode 100644 2025/project/app/core/logging.py create mode 100644 2025/project/app/db/schema.py create mode 100644 2025/project/app/main.py create mode 100644 2025/project/app/models/user.py create mode 100644 2025/project/app/services/user_service.py create mode 100644 2025/project/docker-compose.yaml create mode 100644 2025/project/pyproject.toml create mode 100644 2025/project/tests/api/v1/test_user.py create mode 100644 2025/project/tests/test_db.py create mode 100644 2025/project/uv.lock diff --git a/2025/project/.env.example b/2025/project/.env.example new file mode 100644 index 00000000..ae2acab7 --- /dev/null +++ b/2025/project/.env.example @@ -0,0 +1,2 @@ +db_user=your_db_user +db_password=your_db_password \ No newline at end of file diff --git a/2025/project/.python-version b/2025/project/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/2025/project/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/2025/project/Dockerfile b/2025/project/Dockerfile new file mode 100644 index 00000000..84147e4f --- /dev/null +++ b/2025/project/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.13-slim-bookworm + +RUN apt-get update && apt-get install --no-install-recommends -y \ + build-essential && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +ADD https://astral.sh/uv/install.sh /install.sh +RUN chmod -R 755 /install.sh && /install.sh && rm /install.sh + +# Set up the UV environment path correctly +ENV PATH="/root/.local/bin:${PATH}" + +WORKDIR /app + +COPY . . + +RUN uv sync + +ENV PATH="/app/.venv/bin:{$PATH}" + +# Expose the specified port for FastAPI +EXPOSE $PORT + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file diff --git a/2025/project/README.md b/2025/project/README.md new file mode 100644 index 00000000..90097a92 --- /dev/null +++ b/2025/project/README.md @@ -0,0 +1,39 @@ +Starting the server: + +uv run uvicorn app.main:app --reload + +Example requests: + +Hereโ€™s a set of simple curl examples you can use to interact with your FastAPI app once itโ€™s running (default at http://localhost:8000): + +1๏ธโƒฃ Create a User + +curl -X POST "http://localhost:8000/api/v1/users" \ + -H "Content-Type: application/json" \ + -d '{"name": "Ada Lovelace"}' + + +2๏ธโƒฃ Get All Users + +curl -X GET "http://localhost:8000/api/v1/users" + + +3๏ธโƒฃ Get a User by ID + +(Replace 1 with the actual ID from the create response) + +curl -X GET "http://localhost:8000/api/v1/users/1" + + +4๏ธโƒฃ Update a User + +curl -X PUT "http://localhost:8000/api/v1/users/1" \ + -H "Content-Type: application/json" \ + -d '{"name": "Grace Hopper"}' + + +โธป + +5๏ธโƒฃ Delete a User + +curl -X DELETE "http://localhost:8000/api/v1/users/1" \ No newline at end of file diff --git a/2025/project/app/api/v1/user.py b/2025/project/app/api/v1/user.py new file mode 100644 index 00000000..565afbdc --- /dev/null +++ b/2025/project/app/api/v1/user.py @@ -0,0 +1,47 @@ +from fastapi import APIRouter, Depends, HTTPException + +from app.db.schema import SessionLocal +from app.models.user import UserCreate, UserRead +from app.services.user_service import UserService + +router = APIRouter() + + +def get_user_service() -> UserService: + return UserService(session=SessionLocal()) + + +@router.get("/users", response_model=list[UserRead]) +def get_users(service: UserService = Depends(get_user_service)): + return service.list_users() + + +@router.post("/users", response_model=UserRead) +def create_user(user: UserCreate, service: UserService = Depends(get_user_service)): + return service.create_user(user.name) + + +@router.get("/users/{user_id}", response_model=UserRead) +def get_user(user_id: int, service: UserService = Depends(get_user_service)): + user = service.get_user(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +@router.put("/users/{user_id}", response_model=UserRead) +def update_user( + user_id: int, user: UserCreate, service: UserService = Depends(get_user_service) +): + updated = service.update_user(user_id, user.name) + if not updated: + raise HTTPException(status_code=404, detail="User not found") + return updated + + +@router.delete("/users/{user_id}") +def delete_user(user_id: int, service: UserService = Depends(get_user_service)): + success = service.delete_user(user_id) + if not success: + raise HTTPException(status_code=404, detail="User not found") + return {"success": True} diff --git a/2025/project/app/core/config.py b/2025/project/app/core/config.py new file mode 100644 index 00000000..e5737153 --- /dev/null +++ b/2025/project/app/core/config.py @@ -0,0 +1,19 @@ +from dotenv import load_dotenv +from pydantic_settings import BaseSettings + +load_dotenv() + + +class Config(BaseSettings): + app_name: str = "ScalableFastAPIProject" + debug: bool = False + db_user: str = "" + db_password: str = "" + db_name: str = "test.db" + + @property + def db_url(self): + return f"sqlite:///./{self.db_name}" + + +config = Config() diff --git a/2025/project/app/core/logging.py b/2025/project/app/core/logging.py new file mode 100644 index 00000000..3d787a52 --- /dev/null +++ b/2025/project/app/core/logging.py @@ -0,0 +1,7 @@ +import logging + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s [%(name)s] %(message)s" + ) \ No newline at end of file diff --git a/2025/project/app/db/schema.py b/2025/project/app/db/schema.py new file mode 100644 index 00000000..830d39e5 --- /dev/null +++ b/2025/project/app/db/schema.py @@ -0,0 +1,18 @@ +from sqlalchemy import String, create_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker + +from app.core.config import config + +engine = create_engine(config.db_url, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +class Base(DeclarativeBase): + pass + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String, index=True) diff --git a/2025/project/app/main.py b/2025/project/app/main.py new file mode 100644 index 00000000..dd88371b --- /dev/null +++ b/2025/project/app/main.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI + +from app.api.v1 import user +from app.core.config import config +from app.core.logging import setup_logging +from app.db.schema import Base, engine + +setup_logging() +Base.metadata.create_all(bind=engine) + +app = FastAPI(title=config.app_name) + + +# Register routes +app.include_router(user.router, prefix="/api/v1") diff --git a/2025/project/app/models/user.py b/2025/project/app/models/user.py new file mode 100644 index 00000000..c37604ec --- /dev/null +++ b/2025/project/app/models/user.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class UserCreate(BaseModel): + name: str + + +class UserRead(BaseModel): + id: int + name: str diff --git a/2025/project/app/services/user_service.py b/2025/project/app/services/user_service.py new file mode 100644 index 00000000..c0cf44bd --- /dev/null +++ b/2025/project/app/services/user_service.py @@ -0,0 +1,38 @@ +from sqlalchemy.orm import Session + +from app.db.schema import User + + +class UserService: + def __init__(self, session: Session): + self._db = session + + def list_users(self) -> list[User]: + return self._db.query(User).all() + + def get_user(self, user_id: int) -> User | None: + return self._db.query(User).filter(User.id == user_id).first() + + def create_user(self, name: str) -> User: + user = User(name=name) + self._db.add(user) + self._db.commit() + self._db.refresh(user) + return user + + def update_user(self, user_id: int, name: str) -> User | None: + user = self.get_user(user_id) + if not user: + return None + user.name = name + self._db.commit() + self._db.refresh(user) + return user + + def delete_user(self, user_id: int) -> bool: + user = self.get_user(user_id) + if not user: + return False + self._db.delete(user) + self._db.commit() + return True diff --git a/2025/project/docker-compose.yaml b/2025/project/docker-compose.yaml new file mode 100644 index 00000000..856a1263 --- /dev/null +++ b/2025/project/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + web: + build: . + ports: + - "8000:80" + environment: + - DB_USER=admin + - DB_PASSWORD=secret + - DB_NAME=test.db \ No newline at end of file diff --git a/2025/project/pyproject.toml b/2025/project/pyproject.toml new file mode 100644 index 00000000..b74fbd7f --- /dev/null +++ b/2025/project/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "recording" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.116.1", + "httpx>=0.28.1", + "pydantic-settings>=2.10.1", + "pytest>=8.4.1", + "python-dotenv>=1.1.1", + "sqlalchemy>=2.0.42", + "uvicorn>=0.35.0", +] + +[tool.pytest.ini_options] +pythonpath = "." \ No newline at end of file diff --git a/2025/project/tests/api/v1/test_user.py b/2025/project/tests/api/v1/test_user.py new file mode 100644 index 00000000..781e66b4 --- /dev/null +++ b/2025/project/tests/api/v1/test_user.py @@ -0,0 +1,34 @@ +from fastapi.testclient import TestClient + +from app.api.v1.user import get_user_service +from app.main import app +from app.services.user_service import UserService +from tests.test_db import TestingSessionLocal + +# Setup the TestClient +client = TestClient(app) + + +# Dependency to override the get_db dependency in the main app +def override_get_user_service(): + session = TestingSessionLocal() + yield UserService(session=session) + + +app.dependency_overrides[get_user_service] = override_get_user_service + + +def test_create_and_get_user(): + # Create a new user + response = client.post("/api/v1/users", json={"name": "Test User"}) + assert response.status_code == 200 + created_user = response.json() + assert created_user["name"] == "Test User" + assert "id" in created_user + + # Fetch the same user + get_response = client.get(f"/api/v1/users/{created_user['id']}") + assert get_response.status_code == 200 + fetched_user = get_response.json() + assert fetched_user["id"] == created_user["id"] + assert fetched_user["name"] == "Test User" diff --git a/2025/project/tests/test_db.py b/2025/project/tests/test_db.py new file mode 100644 index 00000000..6c0c3113 --- /dev/null +++ b/2025/project/tests/test_db.py @@ -0,0 +1,16 @@ +from sqlalchemy import StaticPool, create_engine +from sqlalchemy.orm import sessionmaker + +from app.db.schema import Base + +# Setup the in-memory SQLite database for testing +DATABASE_URL = "sqlite:///:memory:" +engine = create_engine( + DATABASE_URL, + connect_args={ + "check_same_thread": False, + }, + poolclass=StaticPool, +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base.metadata.create_all(bind=engine) diff --git a/2025/project/uv.lock b/2025/project/uv.lock new file mode 100644 index 00000000..a622bd21 --- /dev/null +++ b/2025/project/uv.lock @@ -0,0 +1,369 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "inject" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/b6/762d2dac6b587698abde000dc0fe85dfe9cd469cbda8856699a84adee82a/inject-5.3.0.tar.gz", hash = "sha256:bc9db0fe05a42990dbdaf570db085c409bc8d1a9dea6d06143049476b922abba", size = 26482, upload-time = "2025-06-20T10:20:51.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/6e/b00ef8fe9a43aa3a6f5687b710832f0d876c0812bd0ce1c3af3e71bf7dd1/inject-5.3.0-py2.py3-none-any.whl", hash = "sha256:4758eb6c464d3e2badbbf65ac991c64752b05429d6af4c3c0e5b2765efaf7e73", size = 14349, upload-time = "2025-06-20T10:20:50.717Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "recording" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "inject" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "inject", specifier = ">=5.3.0" }, + { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, + { name = "sqlalchemy", specifier = ">=2.0.42" }, + { name = "uvicorn", specifier = ">=0.35.0" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" }, + { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] From 910f9e68e064eddfc5c724bda11baf326e300acd Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 11 Sep 2025 16:26:56 +0200 Subject: [PATCH 043/113] Added Python 3.14 code examples. --- 2025/python314/annot.py | 17 +++++++ 2025/python314/compress.py | 67 +++++++++++++++++++++++++ 2025/python314/exception_parenthesis.py | 18 +++++++ 2025/python314/pyproject.toml | 7 +++ 2025/python314/template_strings.py | 42 ++++++++++++++++ 2025/python314/uv.lock | 8 +++ 6 files changed, 159 insertions(+) create mode 100644 2025/python314/annot.py create mode 100644 2025/python314/compress.py create mode 100644 2025/python314/exception_parenthesis.py create mode 100644 2025/python314/pyproject.toml create mode 100644 2025/python314/template_strings.py create mode 100644 2025/python314/uv.lock diff --git a/2025/python314/annot.py b/2025/python314/annot.py new file mode 100644 index 00000000..6faa8ce0 --- /dev/null +++ b/2025/python314/annot.py @@ -0,0 +1,17 @@ +import annotationlib + +# You no longer need string annotations or `from __future__ import annotations` +class Node: + def __init__(self, value: int, next: Node | None = None): # This works fine in Python 3.14 + self.value = value + self.next = next + +def my_function(x: int, y: Node | None) -> bool: + return x > 0 and y is not None + +def main() -> None: + sig = annotationlib.get_annotations(my_function) + print(sig) + +if __name__ == "__main__": + main() diff --git a/2025/python314/compress.py b/2025/python314/compress.py new file mode 100644 index 00000000..b6991727 --- /dev/null +++ b/2025/python314/compress.py @@ -0,0 +1,67 @@ +import time +import gzip +import bz2 +import compression.zstd as zstd # New in Python 3.14 + +from typing import Callable + +type CompressionFunction = Callable[[bytes], bytes] + + +def benchmark_compression( + name: str, + compress_fn: CompressionFunction, + decompress_fn: CompressionFunction, + data: bytes +) -> None: + """Benchmark compression and decompression performance.""" + # Compress + start = time.perf_counter() + compressed: bytes = compress_fn(data) + compress_time: float = time.perf_counter() - start + + # Decompress + start = time.perf_counter() + decompressed: bytes = decompress_fn(compressed) + decompress_time: float = time.perf_counter() - start + + assert decompressed == data, f"{name} decompressed data mismatch!" + + print( + f"{name:>6} | Size: {len(compressed):7} bytes | " + f"Compress: {compress_time * 1000:7.2f} ms | " + f"Decompress: {decompress_time * 1000:7.2f} ms" + ) + + +def main() -> None: + data: bytes = ( + b"ArjanCodes is the best Python channel on YouTube!" * 50_000 + ) + + print("== Compression Benchmark ==") + + benchmark_compression( + "zstd", + compress_fn=zstd.compress, + decompress_fn=zstd.decompress, + data=data, + ) + + benchmark_compression( + "gzip", + compress_fn=gzip.compress, + decompress_fn=gzip.decompress, + data=data, + ) + + benchmark_compression( + "bz2", + compress_fn=bz2.compress, + decompress_fn=bz2.decompress, + data=data, + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/python314/exception_parenthesis.py b/2025/python314/exception_parenthesis.py new file mode 100644 index 00000000..37b5eeaf --- /dev/null +++ b/2025/python314/exception_parenthesis.py @@ -0,0 +1,18 @@ +def risky_function(value: str) -> int: + if value == "int": + return int("not a number") # raises ValueError + elif value == "key": + return {"a": 1}["b"] # raises KeyError + else: + raise RuntimeError("Something else went wrong") + +def main() -> None: + try: + risky_function("key") + except ValueError, KeyError: + print("Caught either ValueError or KeyError") + except RuntimeError as e: + print(f"Caught a runtime error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/python314/pyproject.toml b/2025/python314/pyproject.toml new file mode 100644 index 00000000..25c674d3 --- /dev/null +++ b/2025/python314/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "python314" +version = "0.1.0" +description = "Python 3.14 code example" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2025/python314/template_strings.py b/2025/python314/template_strings.py new file mode 100644 index 00000000..013daa31 --- /dev/null +++ b/2025/python314/template_strings.py @@ -0,0 +1,42 @@ +from string.templatelib import Template +from typing import Literal + + +from html import escape +from string.templatelib import Template, Interpolation + +def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object: + if conversion == "a": + return ascii(value) + elif conversion == "r": + return repr(value) + elif conversion == "s": + return str(value) + return value + +def f(template: Template, sanitize: bool = False) -> str: + parts: list[str] = [] + for item in template: + match item: + case str() as s: + parts.append(s) + case Interpolation(value, _, conversion, format_spec): + value = convert(value, conversion) + value = format(value, format_spec) + if sanitize: + value = escape(str(value)) + parts.append(value) + return "".join(parts) + +def to_html(template: Template) -> str: + return f(template, sanitize=True) + +def main() -> None: + evil = "" + template = t"

{evil}

" + + print(f(template)) + print(to_html(template)) + +if __name__ == "__main__": + main() diff --git a/2025/python314/uv.lock b/2025/python314/uv.lock new file mode 100644 index 00000000..cd2600b2 --- /dev/null +++ b/2025/python314/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "python314" +version = "0.1.0" +source = { virtual = "." } From f8b0868fc7ae337b707787b4d734ee1214bad89f Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 16 Sep 2025 13:42:23 +0200 Subject: [PATCH 044/113] Added uv workspaces example --- 2025/workspaces/packages/api/main.py | 16 + 2025/workspaces/packages/api/pyproject.toml | 10 + 2025/workspaces/packages/cli/main.py | 29 + 2025/workspaces/packages/cli/pyproject.toml | 10 + 2025/workspaces/packages/core/pyproject.toml | 8 + .../workspaces/packages/core/src/core/news.py | 11 + .../packages/summarizer/pyproject.toml | 8 + .../summarizer/src/summarizer/summarize.py | 15 + 2025/workspaces/pyproject.toml | 17 + 2025/workspaces/uv.lock | 497 ++++++++++++++++++ 10 files changed, 621 insertions(+) create mode 100644 2025/workspaces/packages/api/main.py create mode 100644 2025/workspaces/packages/api/pyproject.toml create mode 100644 2025/workspaces/packages/cli/main.py create mode 100644 2025/workspaces/packages/cli/pyproject.toml create mode 100644 2025/workspaces/packages/core/pyproject.toml create mode 100644 2025/workspaces/packages/core/src/core/news.py create mode 100644 2025/workspaces/packages/summarizer/pyproject.toml create mode 100644 2025/workspaces/packages/summarizer/src/summarizer/summarize.py create mode 100644 2025/workspaces/pyproject.toml create mode 100644 2025/workspaces/uv.lock diff --git a/2025/workspaces/packages/api/main.py b/2025/workspaces/packages/api/main.py new file mode 100644 index 00000000..cdb712b2 --- /dev/null +++ b/2025/workspaces/packages/api/main.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +import uvicorn +from core.news import fetch_headlines +import os +from dotenv import load_dotenv + +load_dotenv() # Load environment variables from .env + +app = FastAPI() + +@app.get("/headlines") +def get_headlines(limit: int = 5): + return {"headlines": fetch_headlines(limit)} + +if __name__ == "__main__": + uvicorn.run("main:app", host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", 8000)), reload=True) \ No newline at end of file diff --git a/2025/workspaces/packages/api/pyproject.toml b/2025/workspaces/packages/api/pyproject.toml new file mode 100644 index 00000000..84c5054d --- /dev/null +++ b/2025/workspaces/packages/api/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "api_example" +version = "0.1.0" +description = "FastAPI app using uv workspaces" +requires-python = ">=3.12" +dependencies = [ + "fastapi", + "uvicorn", + +] \ No newline at end of file diff --git a/2025/workspaces/packages/cli/main.py b/2025/workspaces/packages/cli/main.py new file mode 100644 index 00000000..e5db0f65 --- /dev/null +++ b/2025/workspaces/packages/cli/main.py @@ -0,0 +1,29 @@ +import typer +from rich.console import Console +from core.news import fetch_headlines +from summarizer.summarize import summarize_text +import os + + +from dotenv import load_dotenv + +load_dotenv() # Load OPENAI_API_KEY from .env + +app = typer.Typer() +console = Console() + +@app.command() +def headlines(limit: int = 5): + headlines = fetch_headlines(limit) + console.print(f"[bold blue]Top {limit} headlines from Hacker News:[/bold blue]") + for i, title in enumerate(headlines, 1): + console.print(f"[green]{i}.[/green] {title}") + + api_key = os.environ.get("OPENAI_API_KEY") + + console.print("\n[bold blue]Summary:[/bold blue]") + summary = summarize_text("\n".join(headlines), api_key) + console.print(summary) + +if __name__ == "__main__": + app() diff --git a/2025/workspaces/packages/cli/pyproject.toml b/2025/workspaces/packages/cli/pyproject.toml new file mode 100644 index 00000000..47b08db4 --- /dev/null +++ b/2025/workspaces/packages/cli/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "cli_example" +version = "0.1.0" +description = "CLI tool using uv workspaces" +requires-python = ">=3.12" +dependencies = [ + "typer", + "rich", + +] \ No newline at end of file diff --git a/2025/workspaces/packages/core/pyproject.toml b/2025/workspaces/packages/core/pyproject.toml new file mode 100644 index 00000000..815ec2da --- /dev/null +++ b/2025/workspaces/packages/core/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "core" +version = "0.1.0" +description = "News aggregation core library" +requires-python = ">=3.12" +dependencies = [ + "httpx", "beautifulsoup4" +] \ No newline at end of file diff --git a/2025/workspaces/packages/core/src/core/news.py b/2025/workspaces/packages/core/src/core/news.py new file mode 100644 index 00000000..6e840991 --- /dev/null +++ b/2025/workspaces/packages/core/src/core/news.py @@ -0,0 +1,11 @@ +import httpx +from bs4 import BeautifulSoup + +NEWS_URL = "https://news.ycombinator.com" + +def fetch_headlines(limit: int = 5) -> list[str]: + response = httpx.get(NEWS_URL, timeout=10) + response.raise_for_status() + soup = BeautifulSoup(response.text, "html.parser") + titles = [a.get_text() for a in soup.select(".titleline a")] + return titles[:limit] diff --git a/2025/workspaces/packages/summarizer/pyproject.toml b/2025/workspaces/packages/summarizer/pyproject.toml new file mode 100644 index 00000000..a66a5391 --- /dev/null +++ b/2025/workspaces/packages/summarizer/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "summarizer" +version = "0.1.0" +description = "Library for summarizing text using OpenAI" +requires-python = ">=3.12" +dependencies = [ + "openai" +] \ No newline at end of file diff --git a/2025/workspaces/packages/summarizer/src/summarizer/summarize.py b/2025/workspaces/packages/summarizer/src/summarizer/summarize.py new file mode 100644 index 00000000..c3d70b59 --- /dev/null +++ b/2025/workspaces/packages/summarizer/src/summarizer/summarize.py @@ -0,0 +1,15 @@ +from openai import OpenAI + +def summarize_text(text: str, api_key: str) -> str: + client = OpenAI( + # This is the default and can be omitted + api_key=api_key, + ) + + response = client.responses.create( + model="gpt-5", + instructions="You are a text summary writer.", + input=f"Summarize the following text into a single concise paragraph: {text}" + ) + + return response.output_text \ No newline at end of file diff --git a/2025/workspaces/pyproject.toml b/2025/workspaces/pyproject.toml new file mode 100644 index 00000000..2f310e8a --- /dev/null +++ b/2025/workspaces/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "uv_workspace" +version = "0.1.0" +description = "Example of how to use uv workspaces" +requires-python = ">=3.12" +dependencies = [ + "python-dotenv", + "core", + "summarizer" +] + +[tool.uv.sources] +core = { workspace = true } +summarizer = { workspace = true } + +[tool.uv.workspace] +members = [ "packages/*" ] diff --git a/2025/workspaces/uv.lock b/2025/workspaces/uv.lock new file mode 100644 index 00000000..675974a0 --- /dev/null +++ b/2025/workspaces/uv.lock @@ -0,0 +1,497 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[manifest] +members = [ + "api-example", + "cli-example", + "core", + "summarizer", + "uv-workspace", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "api-example" +version = "0.1.0" +source = { virtual = "packages/api" } +dependencies = [ + { name = "fastapi" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi" }, + { name = "uvicorn" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cli-example" +version = "0.1.0" +source = { virtual = "packages/cli" } +dependencies = [ + { name = "rich" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "rich" }, + { name = "typer" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "core" +version = "0.1.0" +source = { editable = "packages/core" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "httpx" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4" }, + { name = "httpx" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jiter" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510, upload-time = "2025-09-15T09:19:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521, upload-time = "2025-09-15T09:19:27.525Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214, upload-time = "2025-09-15T09:19:28.727Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280, upload-time = "2025-09-15T09:19:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895, upload-time = "2025-09-15T09:19:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421, upload-time = "2025-09-15T09:19:32.746Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932, upload-time = "2025-09-15T09:19:34.612Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959, upload-time = "2025-09-15T09:19:35.994Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187, upload-time = "2025-09-15T09:19:37.426Z" }, + { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461, upload-time = "2025-09-15T09:19:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664, upload-time = "2025-09-15T09:19:40.096Z" }, + { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520, upload-time = "2025-09-15T09:19:41.798Z" }, + { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021, upload-time = "2025-09-15T09:19:43.523Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384, upload-time = "2025-09-15T09:19:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389, upload-time = "2025-09-15T09:19:46.094Z" }, + { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519, upload-time = "2025-09-15T09:19:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198, upload-time = "2025-09-15T09:19:49.116Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835, upload-time = "2025-09-15T09:19:50.468Z" }, + { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655, upload-time = "2025-09-15T09:19:51.726Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135, upload-time = "2025-09-15T09:19:53.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063, upload-time = "2025-09-15T09:19:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139, upload-time = "2025-09-15T09:19:55.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369, upload-time = "2025-09-15T09:19:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538, upload-time = "2025-09-15T09:19:58.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737, upload-time = "2025-09-15T09:19:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183, upload-time = "2025-09-15T09:20:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414, upload-time = "2025-09-15T09:20:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223, upload-time = "2025-09-15T09:20:05.631Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306, upload-time = "2025-09-15T09:20:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565, upload-time = "2025-09-15T09:20:08.283Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465, upload-time = "2025-09-15T09:20:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581, upload-time = "2025-09-15T09:20:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102, upload-time = "2025-09-15T09:20:12.175Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477, upload-time = "2025-09-15T09:20:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004, upload-time = "2025-09-15T09:20:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855, upload-time = "2025-09-15T09:20:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802, upload-time = "2025-09-15T09:20:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405, upload-time = "2025-09-15T09:20:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "openai" +version = "1.107.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/24/7fb5749bcf66b52209e3ece05cb4eaeae2102e95f8ae77589e8afaf70ba8/openai-1.107.3.tar.gz", hash = "sha256:69bb8032b05c5f00f7660e422f70f9aabc94793b9a30c5f899360ed21e46314f", size = 564194, upload-time = "2025-09-15T20:09:20.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/1d/58ad0084451f64a9193de48c0afd63047682ffdedb6ae1d494a203e03fd5/openai-1.107.3-py3-none-any.whl", hash = "sha256:4ca54a847235ac04c6320da70fdc06b62d71439de9ec0aa40d5690c3064d4025", size = 947600, upload-time = "2025-09-15T20:09:18.219Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, +] + +[[package]] +name = "summarizer" +version = "0.1.0" +source = { editable = "packages/summarizer" } +dependencies = [ + { name = "openai" }, +] + +[package.metadata] +requires-dist = [{ name = "openai" }] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer" +version = "0.17.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uv-workspace" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "core" }, + { name = "python-dotenv" }, + { name = "summarizer" }, +] + +[package.metadata] +requires-dist = [ + { name = "core", editable = "packages/core" }, + { name = "python-dotenv" }, + { name = "summarizer", editable = "packages/summarizer" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] From ee335f0fcc187ee414d609cd94603797bd6b4d84 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 3 Sep 2025 15:19:19 +0200 Subject: [PATCH 045/113] Added registry code example. --- 2025/registry/README.md | 5 ++ 2025/registry/basic_example.py | 61 +++++++++++++ 2025/registry/commands/__init__.py | 0 2025/registry/commands/text/__init__.py | 0 2025/registry/commands/text/count.py | 8 ++ 2025/registry/commands/text/reverse.py | 8 ++ 2025/registry/main.py | 42 +++++++++ 2025/registry/plugins/__init__.py | 0 2025/registry/plugins/shout.py | 7 ++ 2025/registry/pyproject.toml | 8 ++ 2025/registry/registry.py | 13 +++ 2025/registry/uv.lock | 115 ++++++++++++++++++++++++ 12 files changed, 267 insertions(+) create mode 100644 2025/registry/README.md create mode 100644 2025/registry/basic_example.py create mode 100644 2025/registry/commands/__init__.py create mode 100644 2025/registry/commands/text/__init__.py create mode 100644 2025/registry/commands/text/count.py create mode 100644 2025/registry/commands/text/reverse.py create mode 100644 2025/registry/main.py create mode 100644 2025/registry/plugins/__init__.py create mode 100644 2025/registry/plugins/shout.py create mode 100644 2025/registry/pyproject.toml create mode 100644 2025/registry/registry.py create mode 100644 2025/registry/uv.lock diff --git a/2025/registry/README.md b/2025/registry/README.md new file mode 100644 index 00000000..7fbccd29 --- /dev/null +++ b/2025/registry/README.md @@ -0,0 +1,5 @@ +Examples of commands: + +- uv run main.py text count "This is a registry pattern example" +- uv run main.py text shout "plugin power" +- uv run main.py text reverse "Was it a car or a cat I saw" \ No newline at end of file diff --git a/2025/registry/basic_example.py b/2025/registry/basic_example.py new file mode 100644 index 00000000..71cb5431 --- /dev/null +++ b/2025/registry/basic_example.py @@ -0,0 +1,61 @@ +from functools import wraps +from typing import Any, Callable + +type Data = dict[str, Any] +type ExportFn = Callable[[Data], None] + +# The registry: maps format name to export function +exporters: dict[str, ExportFn] = {} + + +def register_exporter(name: str): + def decorator(func: ExportFn): + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return func(*args, **kwargs) + + exporters[name] = wrapper + return wrapper + + return decorator + + +@register_exporter("pdf") +def export_pdf(data: Data) -> None: + print(f"Exporting data to PDF: {data}") + + +@register_exporter("csv") +def export_csv(data: Data) -> None: + print(f"Exporting data to CSV: {data}") + + +@register_exporter("json") +def export_json(data: Data) -> None: + import json + + print("Exporting data to JSON:") + print(json.dumps(data, indent=2)) + + +def export_data(data: Data, format: str) -> None: + exporter = exporters.get(format) + if exporter is None: + raise ValueError(f"โŒ No exporter found for format: {format}") + exporter(data) + + +def main() -> None: + sample_data: Data = {"name": "Alice", "age": 30} + + # Try exporting in different formats + export_data(sample_data, "pdf") + export_data(sample_data, "csv") + export_data(sample_data, "json") + + # This would raise an error: + export_data(sample_data, "xlsx") + + +if __name__ == "__main__": + main() diff --git a/2025/registry/commands/__init__.py b/2025/registry/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/registry/commands/text/__init__.py b/2025/registry/commands/text/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/registry/commands/text/count.py b/2025/registry/commands/text/count.py new file mode 100644 index 00000000..7da82f82 --- /dev/null +++ b/2025/registry/commands/text/count.py @@ -0,0 +1,8 @@ +from registry import register_command +from rich import print + + +@register_command("text", "count") +def count_words(text: str) -> None: + word_count = len(text.split()) + print(f"[green]Word count:[/green] [bold]{word_count}[/bold]") diff --git a/2025/registry/commands/text/reverse.py b/2025/registry/commands/text/reverse.py new file mode 100644 index 00000000..f3657d5b --- /dev/null +++ b/2025/registry/commands/text/reverse.py @@ -0,0 +1,8 @@ +from registry import register_command +from rich import print + + +@register_command("text", "reverse") +def reverse_text(text: str) -> None: + reversed_text = text[::-1] + print(f"[cyan]Reversed:[/cyan] [bold]{reversed_text}[/bold]") diff --git a/2025/registry/main.py b/2025/registry/main.py new file mode 100644 index 00000000..ee151fb3 --- /dev/null +++ b/2025/registry/main.py @@ -0,0 +1,42 @@ +import importlib +import pkgutil + +import typer +from registry import get_registry + +app = typer.Typer() + + +def load_text_commands() -> None: + import commands.text + + for _, module_name, _ in pkgutil.iter_modules(commands.text.__path__): + importlib.import_module(f"commands.text.{module_name}") + + +def load_plugins() -> None: + import plugins + + for _, module_name, _ in pkgutil.iter_modules(plugins.__path__): + importlib.import_module(f"plugins.{module_name}") + + +def register_with_typer() -> None: + group_apps: dict[str, typer.Typer] = {} + + for group, name, func in get_registry(): + if group not in group_apps: + group_apps[group] = typer.Typer() + app.add_typer(group_apps[group], name=group) + group_apps[group].command(name)(func) + + +def main() -> None: + load_text_commands() + load_plugins() + register_with_typer() + app() + + +if __name__ == "__main__": + main() diff --git a/2025/registry/plugins/__init__.py b/2025/registry/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2025/registry/plugins/shout.py b/2025/registry/plugins/shout.py new file mode 100644 index 00000000..743fe5f4 --- /dev/null +++ b/2025/registry/plugins/shout.py @@ -0,0 +1,7 @@ +from registry import register_command +from rich import print + + +@register_command("text", "shout") +def shout(text: str) -> None: + print(f"[bold red]{text.upper()}!!![/bold red]") diff --git a/2025/registry/pyproject.toml b/2025/registry/pyproject.toml new file mode 100644 index 00000000..7a0c659c --- /dev/null +++ b/2025/registry/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "registry" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "rich>=14.1.0", + "typer>=0.17.3", +] diff --git a/2025/registry/registry.py b/2025/registry/registry.py new file mode 100644 index 00000000..b7dee79e --- /dev/null +++ b/2025/registry/registry.py @@ -0,0 +1,13 @@ + +from typing import Callable + +_registry: list[tuple[str, str, Callable[..., None]]] = [] + +def register_command(group: str, name: str): + def decorator(func: Callable[..., None]): + _registry.append((group, name, func)) + return func + return decorator + +def get_registry() -> list[tuple[str, str, Callable[..., None]]]: + return _registry.copy() diff --git a/2025/registry/uv.lock b/2025/registry/uv.lock new file mode 100644 index 00000000..73251388 --- /dev/null +++ b/2025/registry/uv.lock @@ -0,0 +1,115 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "registry" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "rich" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "rich", specifier = ">=14.1.0" }, + { name = "typer", specifier = ">=0.17.3" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "typer" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/82/f4bfed3bc18c6ebd6f828320811bbe4098f92a31adf4040bee59c4ae02ea/typer-0.17.3.tar.gz", hash = "sha256:0c600503d472bcf98d29914d4dcd67f80c24cc245395e2e00ba3603c9332e8ba", size = 103517, upload-time = "2025-08-30T12:35:24.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl", hash = "sha256:643919a79182ab7ac7581056d93c6a2b865b026adf2872c4d02c72758e6f095b", size = 46494, upload-time = "2025-08-30T12:35:22.391Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 8c0cc62d43de64eb4b0ee6e385bd7c4f72ac87b5 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 9 Sep 2025 15:02:14 +0200 Subject: [PATCH 046/113] Added context object example. --- 2025/context/.python-version | 1 + 2025/context/before.py | 38 ++++++++++++++++++ 2025/context/context_v1.py | 57 ++++++++++++++++++++++++++ 2025/context/context_v2.py | 78 ++++++++++++++++++++++++++++++++++++ 2025/context/db.py | 34 ++++++++++++++++ 2025/context/pyproject.toml | 8 ++++ 2025/context/uv.lock | 68 +++++++++++++++++++++++++++++++ 7 files changed, 284 insertions(+) create mode 100644 2025/context/.python-version create mode 100644 2025/context/before.py create mode 100644 2025/context/context_v1.py create mode 100644 2025/context/context_v2.py create mode 100644 2025/context/db.py create mode 100644 2025/context/pyproject.toml create mode 100644 2025/context/uv.lock diff --git a/2025/context/.python-version b/2025/context/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/2025/context/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/2025/context/before.py b/2025/context/before.py new file mode 100644 index 00000000..ec5aaa91 --- /dev/null +++ b/2025/context/before.py @@ -0,0 +1,38 @@ +import logging +from db import db_session, init_db, Article, Session + +# ----------------------------------- +# Application Logic +# ----------------------------------- + +def render_article(article_id: int, db: Session, logger: logging.Logger, api_key: str) -> str: + article = db.query(Article).filter(Article.id == article_id).first() + if not article: + logger.error(f"Article {article_id} not found.") + return "

Article not found.

" + + logger.info(f"Rendering article {article_id} using API key {api_key[:4]}...") + html = f"

{article.title}

{article.body}

" + return html + +def main() -> None: + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger("app") + + init_db() + + with db_session() as session: + + api_key = "abcdef123456" + + html = render_article(1, session, logger, api_key) + print(html) + + html = render_article(999, session, logger, api_key) # not found + print(html) + + +if __name__ == "__main__": + main() + + diff --git a/2025/context/context_v1.py b/2025/context/context_v1.py new file mode 100644 index 00000000..550eff0c --- /dev/null +++ b/2025/context/context_v1.py @@ -0,0 +1,57 @@ +import logging +from dataclasses import dataclass +from typing import Any +from db import db_session, init_db, Article , Session + +# ----------------------------------- +# Context Object +# ----------------------------------- + +@dataclass +class AppContext: + user_id: int + db: Session + logger: logging.Logger + config: dict[str, Any] + +# ----------------------------------- +# Application Logic +# ----------------------------------- + +def render_article(article_id: int, context: AppContext) -> str: + article = context.db.query(Article).filter(Article.id == article_id).first() + if not article: + context.logger.error(f"Article {article_id} not found.") + return "

Article not found.

" + + context.logger.info(f"Rendering article {article_id}") + html = f"

{article.title}

{article.body}

" + return html +# ----------------------------------- +# Entry Point +# ----------------------------------- + +def main() -> None: + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger("app") + + init_db() + + with db_session() as session: + context = AppContext( + user_id=42, + db=session, + logger=logger, + config={"api_key": "abcdef123456"}, + ) + + html = render_article(1, context) + print(html) + + html = render_article(999, context) # not found + print(html) + +if __name__ == "__main__": + main() + + diff --git a/2025/context/context_v2.py b/2025/context/context_v2.py new file mode 100644 index 00000000..1bc45b51 --- /dev/null +++ b/2025/context/context_v2.py @@ -0,0 +1,78 @@ +import logging +from dataclasses import dataclass +from typing import Protocol, Any +from db import db_session, init_db, Article + +# ----------------------------------- +# Protocols for loose coupling +# ----------------------------------- + +class LoggerProtocol(Protocol): + def info(self, msg: str, *args: Any, **kwargs: Any) -> None: ... + def error(self, msg: str, *args: Any, **kwargs: Any) -> None: ... + +class DBProtocol(Protocol): + def query(self, *args: Any, **kwargs: Any) -> Any: ... + +# ----------------------------------- +# Context Object +# ----------------------------------- + +@dataclass +class AppContext: + user_id: int + db: DBProtocol + logger: LoggerProtocol + config: dict[str, Any] + +# ----------------------------------- +# Application Logic +# ----------------------------------- + +def render_article(article_id: int, db: DBProtocol, logger: LoggerProtocol) -> str: + article = db.query(Article).filter(Article.id == article_id).first() + if not article: + logger.error(f"Article {article_id} not found.") + return "

Article not found.

" + + logger.info(f"Rendering article {article_id}") + html = f"

{article.title}

{article.body}

" + return html + +def send_to_external_service(html: str, api_key: str) -> None: + print(f"Sending to API with key {api_key[:4]}... Content: {html[:30]}...") + +def publish_article(article_id: int, context: AppContext) -> None: + html = render_article( + article_id, + db=context.db, + logger=context.logger, + ) + send_to_external_service(html, context.config['api_key']) + +# ----------------------------------- +# Entry Point +# ----------------------------------- + +def main() -> None: + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger("app") + + init_db() + + with db_session() as session: + context = AppContext( + user_id=42, + db=session, + logger=logger, + config={"api_key": "abcdef123456"}, + ) + + publish_article(1, context) + publish_article(2, context) + publish_article(999, context) # Not found example + +if __name__ == "__main__": + main() + + diff --git a/2025/context/db.py b/2025/context/db.py new file mode 100644 index 00000000..cbd8b57f --- /dev/null +++ b/2025/context/db.py @@ -0,0 +1,34 @@ +from contextlib import contextmanager +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.orm import Session, declarative_base, sessionmaker +from typing import Generator + +Base = declarative_base() + +class Article(Base): + __tablename__ = "articles" + + id = Column(Integer, primary_key=True) + title = Column(String) + body = Column(String) + +# SQLite in-memory DB for demo purposes +engine = create_engine("sqlite:///:memory:", echo=False, future=True) +SessionLocal = sessionmaker(bind=engine, expire_on_commit=False) + +def init_db() -> None: + Base.metadata.create_all(bind=engine) + with db_session() as session: + session.add_all([ + Article(id=1, title="Hello", body="World!"), + Article(id=2, title="Python", body="Is Awesome"), + ]) + session.commit() + +@contextmanager +def db_session() -> Generator[Session, None, None]: + session = SessionLocal() + try: + yield session + finally: + session.close() \ No newline at end of file diff --git a/2025/context/pyproject.toml b/2025/context/pyproject.toml new file mode 100644 index 00000000..b700562f --- /dev/null +++ b/2025/context/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "context-object-example" +version = "0.1.0" +description = "An example of the Context Object Pattern using SQLite and SQLAlchemy" +requires-python = ">=3.13" +dependencies = [ + "sqlalchemy>=2.0", +] diff --git a/2025/context/uv.lock b/2025/context/uv.lock new file mode 100644 index 00000000..287b1941 --- /dev/null +++ b/2025/context/uv.lock @@ -0,0 +1,68 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "context-object-example" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [{ name = "sqlalchemy", specifier = ">=2.0" }] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 7f0e3b0550c680e7389fcc0569e060963b3c1273 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 8 Oct 2025 16:53:53 +0200 Subject: [PATCH 047/113] Added DI examples. --- 2025/di/di_before.py | 37 ++++++++ 2025/di/di_pipeline.py | 101 ++++++++++++++++++++ 2025/di/fastapi_app.py | 39 ++++++++ 2025/di/pyproject.toml | 8 ++ 2025/di/uv.lock | 208 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 393 insertions(+) create mode 100644 2025/di/di_before.py create mode 100644 2025/di/di_pipeline.py create mode 100644 2025/di/fastapi_app.py create mode 100644 2025/di/pyproject.toml create mode 100644 2025/di/uv.lock diff --git a/2025/di/di_before.py b/2025/di/di_before.py new file mode 100644 index 00000000..f1c6aa34 --- /dev/null +++ b/2025/di/di_before.py @@ -0,0 +1,37 @@ +import json +from typing import Any + +type Data = list[dict[str, Any]] + +class DataPipeline: + def run(self) -> None: + # Hardcoded loader + data = self._load_data_from_csv() + + # Hardcoded transformation + cleaned = [row for row in data if row["age"] is not None] + + # Hardcoded export + self._export_to_json(cleaned) + + def _load_data_from_csv(self) -> Data: + # Simulate reading from CSV + return [ + {"name": "Arjan", "age": 37}, + {"name": "Jane", "age": None}, + {"name": "Bob", "age": 45}, + ] + + def _export_to_json(self, data: Data) -> None: + with open("output.json", "w") as f: + json.dump(data, f, indent=2) + + +def main() -> None: + pipeline = DataPipeline() + pipeline.run() + print("Pipeline completed. Output written to output.json") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/di/di_pipeline.py b/2025/di/di_pipeline.py new file mode 100644 index 00000000..5faf9720 --- /dev/null +++ b/2025/di/di_pipeline.py @@ -0,0 +1,101 @@ +from typing import Protocol, Callable, Any +import json + +type Data = list[dict[str, Any]] + +# === Interfaces === +class DataLoader(Protocol): + def load(self) -> Data: ... + + +class Transformer(Protocol): + def transform(self, data: Data) -> Data: ... + + +class Exporter(Protocol): + def export(self, data: Data) -> None: ... + + +# === Concrete implementations === +class InMemoryLoader: + def load(self) -> Data: + return [ + {"name": "Arjan", "age": 37}, + {"name": "Jane", "age": None}, + {"name": "Bob", "age": 45}, + ] + + +class CleanMissingFields: + def transform(self, data: Data) -> Data: + return [row for row in data if row["age"] is not None] + + +class JSONExporter: + def __init__(self, filename: str): + self.filename = filename + + def export(self, data: Data) -> None: + with open(self.filename, "w") as f: + json.dump(data, f, indent=2) + + +# === Pipeline === +class DataPipeline: + def __init__(self, loader: DataLoader, transformer: Transformer, exporter: Exporter): + self.loader = loader + self.transformer = transformer + self.exporter = exporter + + def run(self) -> None: + data = self.loader.load() + clean = self.transformer.transform(data) + self.exporter.export(clean) + + +# === Simple DI container === +class Container: + def __init__(self) -> None: + self._providers: dict[str, tuple[Callable[[], Any], bool]] = {} + self._singletons: dict[str, Any] = {} + + def register(self, name: str, provider: Callable[[], Any], singleton: bool = False) -> None: + self._providers[name] = (provider, singleton) + + def resolve(self, name: str) -> Any: + if name in self._singletons: + return self._singletons[name] + + if name not in self._providers: + raise ValueError(f"No provider registered for '{name}'") + + provider, singleton = self._providers[name] + instance = provider() + + if singleton: + self._singletons[name] = instance + + return instance + + +# === Main runner === +def main() -> None: + container = Container() + + container.register("loader", lambda: InMemoryLoader(), singleton=True) + container.register("transformer", lambda: CleanMissingFields()) + container.register("exporter", lambda: JSONExporter("output.json")) + + container.register("pipeline", lambda: DataPipeline( + loader=container.resolve("loader"), + transformer=container.resolve("transformer"), + exporter=container.resolve("exporter"), + )) + + pipeline: DataPipeline = container.resolve("pipeline") + pipeline.run() + print("Pipeline finished. Output written to output.json") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/di/fastapi_app.py b/2025/di/fastapi_app.py new file mode 100644 index 00000000..f927f7e0 --- /dev/null +++ b/2025/di/fastapi_app.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, Depends +from typing import Protocol, Any +import uvicorn + +type Data = list[dict[str, Any]] + +app = FastAPI() + + +# === Transformer interface and implementation === +class Transformer(Protocol): + def transform(self, data: Data) -> Data: ... + + +class CleanMissingFields: + def transform(self, data: Data) -> Data: + return [row for row in data if row["age"] is not None] + + +# === Dependency === +def get_transformer() -> Transformer: + return CleanMissingFields() + + +# === Endpoint === +@app.post("/process/") +def process_data( + data: Data, + transformer: Transformer = Depends(get_transformer) +): + cleaned = transformer.transform(data) + return {"cleaned": cleaned} + +def main(): + uvicorn.run("fastapi_app:app", host="0.0.0.0", port=8000, reload=True) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/di/pyproject.toml b/2025/di/pyproject.toml new file mode 100644 index 00000000..69a304fc --- /dev/null +++ b/2025/di/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "di" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "fastapi", + "uvicorn", +] diff --git a/2025/di/uv.lock b/2025/di/uv.lock new file mode 100644 index 00000000..752e956e --- /dev/null +++ b/2025/di/uv.lock @@ -0,0 +1,208 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "di" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi" }, + { name = "uvicorn" }, +] + +[[package]] +name = "fastapi" +version = "0.118.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/1b/6cbc5bc6d7a07a506c2275d443e4517adb4e02ab42e0a6486568e1749896/fastapi-0.118.1.tar.gz", hash = "sha256:063f9d4ff5bcdfd1ef6e4e6b44ed5fb5f4bf370b39cdce1c9aed22413c371cfe", size = 311185, upload-time = "2025-10-08T09:07:24.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/29/9bf43b2fe09854a4c743eed5ef9b6a84bab39b05bfd46aea7ce6c7bf97f1/fastapi-0.118.1-py3-none-any.whl", hash = "sha256:be88c15c995464d14d2be1d7059860551aeffb9df889688bcea7050c9635badf", size = 97933, upload-time = "2025-10-08T09:07:22.003Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, + { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" }, + { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" }, + { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" }, + { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +] From bdecb71023e4013068a4a9237cb8782b38b1ea65 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 8 Oct 2025 16:57:59 +0200 Subject: [PATCH 048/113] Added intermediate version. Renamed files for clarity. --- 2025/di/{di_before.py => before.py} | 0 2025/di/{di_pipeline.py => container.py} | 0 2025/di/manual.py | 70 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+) rename 2025/di/{di_before.py => before.py} (100%) rename 2025/di/{di_pipeline.py => container.py} (100%) create mode 100644 2025/di/manual.py diff --git a/2025/di/di_before.py b/2025/di/before.py similarity index 100% rename from 2025/di/di_before.py rename to 2025/di/before.py diff --git a/2025/di/di_pipeline.py b/2025/di/container.py similarity index 100% rename from 2025/di/di_pipeline.py rename to 2025/di/container.py diff --git a/2025/di/manual.py b/2025/di/manual.py new file mode 100644 index 00000000..e5f2c534 --- /dev/null +++ b/2025/di/manual.py @@ -0,0 +1,70 @@ +import json +from typing import Protocol, Any + +type Data = list[dict[str, Any]] + + +# === Interfaces === +class DataLoader(Protocol): + def load(self) -> Data: ... + + +class Transformer(Protocol): + def transform(self, data: Data) -> Data: ... + + +class Exporter(Protocol): + def export(self, data: Data) -> None: ... + + +# === Concrete Implementations === +class InMemoryLoader: + def load(self) -> Data: + return [ + {"name": "Arjan", "age": 37}, + {"name": "Jane", "age": None}, + {"name": "Bob", "age": 45}, + ] + + +class CleanMissingFields: + def transform(self, data: Data) -> Data: + return [row for row in data if row["age"] is not None] + + +class JSONExporter: + def __init__(self, filename: str): + self.filename = filename + + def export(self, data: Data) -> None: + with open(self.filename, "w") as f: + json.dump(data, f, indent=2) + + +# === Pipeline === +class DataPipeline: + def __init__(self, loader: DataLoader, transformer: Transformer, exporter: Exporter): + self.loader = loader + self.transformer = transformer + self.exporter = exporter + + def run(self) -> None: + data = self.loader.load() + clean = self.transformer.transform(data) + self.exporter.export(clean) + + +# === Main function: inject dependencies manually === +def main() -> None: + loader = InMemoryLoader() + transformer = CleanMissingFields() + exporter = JSONExporter("output.json") + + pipeline = DataPipeline(loader, transformer, exporter) + pipeline.run() + + print("Pipeline completed. Output written to output.json") + + +if __name__ == "__main__": + main() \ No newline at end of file From d1c8e3141ea42250d12a7138d33c35ae0882f085 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 30 Sep 2025 11:33:19 +0200 Subject: [PATCH 049/113] Added initial code example. --- 2025/event/event.py | 15 +++++++++++++ 2025/event/event_store.py | 44 +++++++++++++++++++++++++++++++++++++++ 2025/event/inventory.py | 40 +++++++++++++++++++++++++++++++++++ 2025/event/main.py | 21 +++++++++++++++++++ 2025/event/pyproject.toml | 6 ++++++ 2025/event/uv.lock | 8 +++++++ 6 files changed, 134 insertions(+) create mode 100644 2025/event/event.py create mode 100644 2025/event/event_store.py create mode 100644 2025/event/inventory.py create mode 100644 2025/event/main.py create mode 100644 2025/event/pyproject.toml create mode 100644 2025/event/uv.lock diff --git a/2025/event/event.py b/2025/event/event.py new file mode 100644 index 00000000..f72f447d --- /dev/null +++ b/2025/event/event.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import StrEnum + + +class EventType(StrEnum): + ITEM_ADDED = "item_added" + ITEM_REMOVED = "item_removed" + + +@dataclass(frozen=True) +class Event: + type: EventType + item: str + timestamp: datetime = datetime.now() diff --git a/2025/event/event_store.py b/2025/event/event_store.py new file mode 100644 index 00000000..6a5358af --- /dev/null +++ b/2025/event/event_store.py @@ -0,0 +1,44 @@ +from collections import Counter +from typing import Optional + +from event import Event, EventType + + +class Snapshot: + def __init__(self, items: list[str], last_event_index: int): + self.items = items + self.last_event_index = last_event_index + + +class EventStore: + def __init__(self, snapshot_interval: int = 5): + self._events: list[Event] = [] + self._snapshot: Optional[Snapshot] = None + self._snapshot_interval = snapshot_interval + + def append(self, event: Event) -> None: + self._events.append(event) + + if self._snapshot_interval and len(self._events) % self._snapshot_interval == 0: + self._create_snapshot() + + def _create_snapshot(self) -> None: + counts = Counter[str]() + for event in self._events: + if event.type == EventType.ITEM_ADDED: + counts[event.item] += 1 + elif event.type == EventType.ITEM_REMOVED: + counts[event.item] -= 1 + + items = [ + item for item, count in counts.items() for _ in range(count) if count > 0 + ] + self._snapshot = Snapshot(items, len(self._events) - 1) + + def get_replay_data(self) -> tuple[list[str], list[Event]]: + if self._snapshot: + start_items = self._snapshot.items + remaining = self._events[self._snapshot.last_event_index + 1 :] + return start_items, remaining + else: + return [], self._events diff --git a/2025/event/inventory.py b/2025/event/inventory.py new file mode 100644 index 00000000..7c9a1064 --- /dev/null +++ b/2025/event/inventory.py @@ -0,0 +1,40 @@ +from collections import Counter +from functools import cache +from event_store import EventStore +from event import Event, EventType + + +class Inventory: + def __init__(self, store: EventStore): + self.store = store + + @cache + def get_items(self) -> list[str]: + start_items, events = self.store.get_replay_data() + counts = Counter(start_items) + + for event in events: + if event.type == EventType.ITEM_ADDED: + counts[event.item] += 1 + elif event.type == EventType.ITEM_REMOVED: + counts[event.item] -= 1 + + return [ + item + for item, count in counts.items() + for _ in range(count) + if count > 0 + ] + + def _invalidate_cache(self) -> None: + self.get_items.cache_clear() + + def add_item(self, item: str) -> None: + self.store.append(Event(EventType.ITEM_ADDED, item)) + self._invalidate_cache() + + def remove_item(self, item: str) -> None: + if item not in self.get_items(): + raise ValueError(f"{item} not in inventory") + self.store.append(Event(EventType.ITEM_REMOVED, item)) + self._invalidate_cache() diff --git a/2025/event/main.py b/2025/event/main.py new file mode 100644 index 00000000..501416d9 --- /dev/null +++ b/2025/event/main.py @@ -0,0 +1,21 @@ +from event_store import EventStore +from inventory import Inventory + + +def main() -> None: + store = EventStore(snapshot_interval=5) + inventory = Inventory(store) + + inventory.add_item("sword") + inventory.add_item("potion") + inventory.add_item("bow") + inventory.add_item("shield") + inventory.add_item("torch") # Triggers snapshot + + inventory.remove_item("potion") + + print(inventory.get_items()) # ['sword', 'bow', 'shield', 'torch'] + + +if __name__ == "__main__": + main() diff --git a/2025/event/pyproject.toml b/2025/event/pyproject.toml new file mode 100644 index 00000000..66f5e217 --- /dev/null +++ b/2025/event/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "event_sourcing" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ +] diff --git a/2025/event/uv.lock b/2025/event/uv.lock new file mode 100644 index 00000000..ecc56cf8 --- /dev/null +++ b/2025/event/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "event-sourcing" +version = "0.1.0" +source = { virtual = "." } From 4137a7f0b55001194d9bab557f5c6d2870875ee2 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 30 Sep 2025 13:10:54 +0200 Subject: [PATCH 050/113] Added get_all_events method --- 2025/event/event_store.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/2025/event/event_store.py b/2025/event/event_store.py index 6a5358af..8879e2f8 100644 --- a/2025/event/event_store.py +++ b/2025/event/event_store.py @@ -42,3 +42,6 @@ def get_replay_data(self) -> tuple[list[str], list[Event]]: return start_items, remaining else: return [], self._events + + def get_all_events(self) -> list[Event]: + return list(self._events) From e0de2b7973fb9cfb5cbd921faedfae82fa069240 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 8 Oct 2025 15:54:20 +0200 Subject: [PATCH 051/113] Updated event example --- 2025/event/event_store.py | 47 --------------------------- 2025/event/{ => v1}/event.py | 4 +-- 2025/event/v1/event_store.py | 14 ++++++++ 2025/event/{ => v1}/inventory.py | 26 +++++++-------- 2025/event/{ => v1}/main.py | 5 ++- 2025/event/v2/event.py | 15 +++++++++ 2025/event/v2/event_store.py | 14 ++++++++ 2025/event/v2/inventory.py | 42 ++++++++++++++++++++++++ 2025/event/v2/item.py | 8 +++++ 2025/event/v2/main.py | 56 ++++++++++++++++++++++++++++++++ 2025/event/v2/projections.py | 21 ++++++++++++ 11 files changed, 187 insertions(+), 65 deletions(-) delete mode 100644 2025/event/event_store.py rename 2025/event/{ => v1}/event.py (88%) create mode 100644 2025/event/v1/event_store.py rename 2025/event/{ => v1}/inventory.py (63%) rename 2025/event/{ => v1}/main.py (80%) create mode 100644 2025/event/v2/event.py create mode 100644 2025/event/v2/event_store.py create mode 100644 2025/event/v2/inventory.py create mode 100644 2025/event/v2/item.py create mode 100644 2025/event/v2/main.py create mode 100644 2025/event/v2/projections.py diff --git a/2025/event/event_store.py b/2025/event/event_store.py deleted file mode 100644 index 8879e2f8..00000000 --- a/2025/event/event_store.py +++ /dev/null @@ -1,47 +0,0 @@ -from collections import Counter -from typing import Optional - -from event import Event, EventType - - -class Snapshot: - def __init__(self, items: list[str], last_event_index: int): - self.items = items - self.last_event_index = last_event_index - - -class EventStore: - def __init__(self, snapshot_interval: int = 5): - self._events: list[Event] = [] - self._snapshot: Optional[Snapshot] = None - self._snapshot_interval = snapshot_interval - - def append(self, event: Event) -> None: - self._events.append(event) - - if self._snapshot_interval and len(self._events) % self._snapshot_interval == 0: - self._create_snapshot() - - def _create_snapshot(self) -> None: - counts = Counter[str]() - for event in self._events: - if event.type == EventType.ITEM_ADDED: - counts[event.item] += 1 - elif event.type == EventType.ITEM_REMOVED: - counts[event.item] -= 1 - - items = [ - item for item, count in counts.items() for _ in range(count) if count > 0 - ] - self._snapshot = Snapshot(items, len(self._events) - 1) - - def get_replay_data(self) -> tuple[list[str], list[Event]]: - if self._snapshot: - start_items = self._snapshot.items - remaining = self._events[self._snapshot.last_event_index + 1 :] - return start_items, remaining - else: - return [], self._events - - def get_all_events(self) -> list[Event]: - return list(self._events) diff --git a/2025/event/event.py b/2025/event/v1/event.py similarity index 88% rename from 2025/event/event.py rename to 2025/event/v1/event.py index f72f447d..adf39e9e 100644 --- a/2025/event/event.py +++ b/2025/event/v1/event.py @@ -9,7 +9,7 @@ class EventType(StrEnum): @dataclass(frozen=True) -class Event: +class Event[T = str]: type: EventType - item: str + data: T timestamp: datetime = datetime.now() diff --git a/2025/event/v1/event_store.py b/2025/event/v1/event_store.py new file mode 100644 index 00000000..c41e68f3 --- /dev/null +++ b/2025/event/v1/event_store.py @@ -0,0 +1,14 @@ + + +from event import Event + + +class EventStore[T]: + def __init__(self): + self._events: list[Event[T]] = [] + + def append(self, event: Event[T]) -> None: + self._events.append(event) + + def get_all_events(self) -> list[Event[T]]: + return list(self._events) diff --git a/2025/event/inventory.py b/2025/event/v1/inventory.py similarity index 63% rename from 2025/event/inventory.py rename to 2025/event/v1/inventory.py index 7c9a1064..30d156b3 100644 --- a/2025/event/inventory.py +++ b/2025/event/v1/inventory.py @@ -1,31 +1,31 @@ from collections import Counter from functools import cache -from event_store import EventStore from event import Event, EventType +from event_store import EventStore class Inventory: - def __init__(self, store: EventStore): + def __init__(self, store: EventStore[str]): self.store = store @cache - def get_items(self) -> list[str]: - start_items, events = self.store.get_replay_data() - counts = Counter(start_items) - - for event in events: + def get_items(self) -> list[tuple[str, int]]: + counts = Counter[str]() + for event in self.store.get_all_events(): if event.type == EventType.ITEM_ADDED: - counts[event.item] += 1 + counts[event.data] += 1 elif event.type == EventType.ITEM_REMOVED: - counts[event.item] -= 1 + counts[event.data] -= 1 return [ - item + (item, count) for item, count in counts.items() - for _ in range(count) if count > 0 ] + def get_count(self, item: str) -> int: + return dict(self.get_items()).get(item, 0) + def _invalidate_cache(self) -> None: self.get_items.cache_clear() @@ -34,7 +34,7 @@ def add_item(self, item: str) -> None: self._invalidate_cache() def remove_item(self, item: str) -> None: - if item not in self.get_items(): + if self.get_count(item) <= 0: raise ValueError(f"{item} not in inventory") self.store.append(Event(EventType.ITEM_REMOVED, item)) - self._invalidate_cache() + self._invalidate_cache() \ No newline at end of file diff --git a/2025/event/main.py b/2025/event/v1/main.py similarity index 80% rename from 2025/event/main.py rename to 2025/event/v1/main.py index 501416d9..0d749b9c 100644 --- a/2025/event/main.py +++ b/2025/event/v1/main.py @@ -3,15 +3,14 @@ def main() -> None: - store = EventStore(snapshot_interval=5) + store = EventStore[str]() inventory = Inventory(store) inventory.add_item("sword") inventory.add_item("potion") inventory.add_item("bow") inventory.add_item("shield") - inventory.add_item("torch") # Triggers snapshot - + inventory.add_item("torch") inventory.remove_item("potion") print(inventory.get_items()) # ['sword', 'bow', 'shield', 'torch'] diff --git a/2025/event/v2/event.py b/2025/event/v2/event.py new file mode 100644 index 00000000..adf39e9e --- /dev/null +++ b/2025/event/v2/event.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import StrEnum + + +class EventType(StrEnum): + ITEM_ADDED = "item_added" + ITEM_REMOVED = "item_removed" + + +@dataclass(frozen=True) +class Event[T = str]: + type: EventType + data: T + timestamp: datetime = datetime.now() diff --git a/2025/event/v2/event_store.py b/2025/event/v2/event_store.py new file mode 100644 index 00000000..c41e68f3 --- /dev/null +++ b/2025/event/v2/event_store.py @@ -0,0 +1,14 @@ + + +from event import Event + + +class EventStore[T]: + def __init__(self): + self._events: list[Event[T]] = [] + + def append(self, event: Event[T]) -> None: + self._events.append(event) + + def get_all_events(self) -> list[Event[T]]: + return list(self._events) diff --git a/2025/event/v2/inventory.py b/2025/event/v2/inventory.py new file mode 100644 index 00000000..4170ed5e --- /dev/null +++ b/2025/event/v2/inventory.py @@ -0,0 +1,42 @@ +from collections import Counter +from functools import cache +from event import Event, EventType +from item import Item +from event_store import EventStore + + +class Inventory: + def __init__(self, store: EventStore[Item]): + self.store = store + + @cache + def get_items(self) -> list[tuple[str, int]]: + counts = Counter[str]() + for event in self.store.get_all_events(): + name = event.data.name + if event.type == EventType.ITEM_ADDED: + counts[name] += 1 + elif event.type == EventType.ITEM_REMOVED: + counts[name] -= 1 + + return [ + (name, count) + for name, count in counts.items() + if count > 0 + ] + + def get_count(self, item_name: str) -> int: + return dict(self.get_items()).get(item_name, 0) + + def _invalidate_cache(self) -> None: + self.get_items.cache_clear() + + def add_item(self, item: Item) -> None: + self.store.append(Event(EventType.ITEM_ADDED, item)) + self._invalidate_cache() + + def remove_item(self, item: Item) -> None: + if self.get_count(item.name) <= 0: + raise ValueError(f"{item.name} not in inventory") + self.store.append(Event(EventType.ITEM_REMOVED, item)) + self._invalidate_cache() \ No newline at end of file diff --git a/2025/event/v2/item.py b/2025/event/v2/item.py new file mode 100644 index 00000000..b0b79c87 --- /dev/null +++ b/2025/event/v2/item.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Item: + name: str + rarity: str + origin: str \ No newline at end of file diff --git a/2025/event/v2/main.py b/2025/event/v2/main.py new file mode 100644 index 00000000..753fdee0 --- /dev/null +++ b/2025/event/v2/main.py @@ -0,0 +1,56 @@ +from event_store import EventStore +from inventory import Inventory +from item import Item +from projections import get_most_collected_items, get_item_origins + + +def main() -> None: + # Create reusable item instances + sword1 = Item(name="sword", rarity="rare", origin="castle") + sword2 = Item(name="sword", rarity="rare", origin="dungeon") + potion1 = Item(name="potion", rarity="common", origin="village") + potion2 = Item(name="potion", rarity="common", origin="village") + bow = Item(name="bow", rarity="uncommon", origin="forest") + torch = Item(name="torch", rarity="common", origin="dungeon") + scroll = Item(name="scroll", rarity="epic", origin="tower") + + # Set up store and inventory + store = EventStore[Item]() + inventory = Inventory(store) + + # Add some items + inventory.add_item(sword1) + inventory.add_item(sword2) + inventory.add_item(potion1) + inventory.add_item(potion2) + inventory.add_item(bow) + inventory.add_item(torch) + inventory.add_item(scroll) + + # Remove some items + inventory.remove_item(potion1) + inventory.remove_item(torch) + + # Show inventory state + print("\n=== Current Inventory ===") + for name, count in inventory.get_items(): + print(f"{name}: {count}") + + print(f"\nSwords in inventory: {inventory.get_count('sword')}") + + # Show projections + events = store.get_all_events() + + print("\n=== Most Collected Items ===") + for name, count in get_most_collected_items(events): + print(f"{name}: {count} collected") + + print("\n=== Item Origins ===") + origins = get_item_origins(events) + for name, origin_set in origins.items(): + origin_list = ', '.join(sorted(origin_set)) + print(f"{name}: {origin_list}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/event/v2/projections.py b/2025/event/v2/projections.py new file mode 100644 index 00000000..1592722a --- /dev/null +++ b/2025/event/v2/projections.py @@ -0,0 +1,21 @@ +from collections import Counter, defaultdict +from event import Event, EventType +from item import Item + + +def get_most_collected_items(events: list[Event[Item]], top_n: int = 5) -> list[tuple[str, int]]: + """Returns a list of the most frequently added item names.""" + counts = Counter[str]() + for event in events: + if event.type == EventType.ITEM_ADDED: + counts[event.data.name] += 1 + return counts.most_common(top_n) + + +def get_item_origins(events: list[Event[Item]]) -> dict[str, set[str]]: + """Returns a mapping of item names to all origins theyโ€™ve appeared in.""" + origins: dict[str, set[str]] = defaultdict(set) + for event in events: + if event.type == EventType.ITEM_ADDED: + origins[event.data.name].add(event.data.origin) + return dict(origins) \ No newline at end of file From d2314e61684b842d88e63080100253c07486f305 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 2 Oct 2025 16:52:13 +0200 Subject: [PATCH 052/113] Added pythonic code example --- 2025/pythonic/01_starting_point.py | 61 ++++++++++++++++++++++ 2025/pythonic/02_functions_not_class.py | 57 ++++++++++++++++++++ 2025/pythonic/03_use_context_manager.py | 55 ++++++++++++++++++++ 2025/pythonic/04_use_eafp.py | 53 +++++++++++++++++++ 2025/pythonic/05_add_type_hints.py | 54 +++++++++++++++++++ 2025/pythonic/05_use_dataclass.py | 47 +++++++++++++++++ 2025/pythonic/06_use_pathlib.py | 48 +++++++++++++++++ 2025/pythonic/07_add_helpers.py | 57 ++++++++++++++++++++ 2025/pythonic/08_add_logging.py | 61 ++++++++++++++++++++++ 2025/pythonic/10_final.py | 69 +++++++++++++++++++++++++ 2025/pythonic/pyproject.toml | 7 +++ 11 files changed, 569 insertions(+) create mode 100644 2025/pythonic/01_starting_point.py create mode 100644 2025/pythonic/02_functions_not_class.py create mode 100644 2025/pythonic/03_use_context_manager.py create mode 100644 2025/pythonic/04_use_eafp.py create mode 100644 2025/pythonic/05_add_type_hints.py create mode 100644 2025/pythonic/05_use_dataclass.py create mode 100644 2025/pythonic/06_use_pathlib.py create mode 100644 2025/pythonic/07_add_helpers.py create mode 100644 2025/pythonic/08_add_logging.py create mode 100644 2025/pythonic/10_final.py create mode 100644 2025/pythonic/pyproject.toml diff --git a/2025/pythonic/01_starting_point.py b/2025/pythonic/01_starting_point.py new file mode 100644 index 00000000..86c4e18b --- /dev/null +++ b/2025/pythonic/01_starting_point.py @@ -0,0 +1,61 @@ +from datetime import datetime +import os + +class FitnessTracker: + def __init__(self): + self.food_log = [] + self.activity_log = [] + + def log_food(self, item, calories, date=None): + if date is None: + date = datetime.now().strftime("%Y-%m-%d") + f = open("food.csv", "a") + f.write(f"{date},{item},{calories}\n") + f.close() + print(f"Appended food: {item} ({calories} kcal) on {date}") + + def log_activity(self, activity, calories_burned, date=None): + if date is None: + from datetime import datetime + date = datetime.now().strftime("%Y-%m-%d") + f = open("activities.csv", "a") + f.write(f"{date},{activity},{calories_burned}\n") + f.close() + print(f"Appended activity: {activity} ({calories_burned} kcal) on {date}") + + def run_day_summary(self, date): + food = [] + if os.path.exists("food.csv"): + f = open("food.csv") + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + food.append(int(parts[2])) + f.close() + else: + print("Could not read food.csv") + + activity = [] + if os.path.exists("activities.csv"): + f = open("activities.csv") + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + activity.append(int(parts[2])) + f.close() + else: + print("Could not read activities.csv") + + food_total = sum(food) + activity_total = sum(activity) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + +tracker = FitnessTracker() +tracker.log_food("Banana", 100) +tracker.log_activity("Running", 300) +tracker.run_day_summary(datetime.now().strftime("%Y-%m-%d")) \ No newline at end of file diff --git a/2025/pythonic/02_functions_not_class.py b/2025/pythonic/02_functions_not_class.py new file mode 100644 index 00000000..4863cf90 --- /dev/null +++ b/2025/pythonic/02_functions_not_class.py @@ -0,0 +1,57 @@ +from datetime import datetime +import os + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def log_food(item, calories, date=None): + if date is None: + date = today() + f = open("food.csv", "a") + f.write(f"{date},{item},{calories}\n") + f.close() + print(f"Appended food: {item} ({calories} kcal) on {date}") + +def log_activity(item, calories, date=None): + if date is None: + date = today() + f = open("activities.csv", "a") + f.write(f"{date},{item},{calories}\n") + f.close() + print(f"Appended activity: {item} ({calories} kcal) on {date}") + +def run_day_summary(date): + food = [] + if os.path.exists("food.csv"): + f = open("food.csv") + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + food.append(int(parts[2])) + f.close() + else: + print("Could not read food.csv") + + activity = [] + if os.path.exists("activities.csv"): + f = open("activities.csv") + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + activity.append(int(parts[2])) + f.close() + else: + print("Could not read activities.csv") + + food_total = sum(food) + activity_total = sum(activity) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/03_use_context_manager.py b/2025/pythonic/03_use_context_manager.py new file mode 100644 index 00000000..6426000b --- /dev/null +++ b/2025/pythonic/03_use_context_manager.py @@ -0,0 +1,55 @@ +from datetime import datetime +import os + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def log_food(item, calories, date=None): + if date is None: + date = today() + with open("food.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended food: {item} ({calories} kcal) on {date}") + +def log_activity(item, calories, date=None): + if date is None: + date = today() + with open("activities.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended activity: {item} ({calories} kcal) on {date}") + +def run_day_summary(date): + food = [] + if os.path.exists("food.csv"): + with open("food.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + food.append(int(parts[2])) + else: + print("Could not read food.csv") + + activity = [] + if os.path.exists("activities.csv"): + with open("activities.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + activity.append(int(parts[2])) + else: + print("Could not read activities.csv") + + + food_total = sum(food) + activity_total = sum(activity) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/04_use_eafp.py b/2025/pythonic/04_use_eafp.py new file mode 100644 index 00000000..99a82146 --- /dev/null +++ b/2025/pythonic/04_use_eafp.py @@ -0,0 +1,53 @@ +from datetime import datetime +import os + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def log_food(item, calories, date=None): + if date is None: + date = today() + with open("food.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended food: {item} ({calories} kcal) on {date}") + +def log_activity(item, calories, date=None): + if date is None: + date = today() + with open("activities.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended activity: {item} ({calories} kcal) on {date}") + +def run_day_summary(date): + food = [] + try: + with open("food.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + food.append(int(parts[2])) + except FileNotFoundError: + print("Could not read food.csv") + + activity = [] + try: + with open("activities.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + activity.append(int(parts[2])) + except FileNotFoundError: + print("Could not read activities.csv") + + food_total = sum(food) + activity_total = sum(activity) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/05_add_type_hints.py b/2025/pythonic/05_add_type_hints.py new file mode 100644 index 00000000..2861a57d --- /dev/null +++ b/2025/pythonic/05_add_type_hints.py @@ -0,0 +1,54 @@ +from datetime import datetime +import os + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def log_food(item: str, calories: int, date: str | None = None) -> None: + if date is None: + date = today() + with open("food.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended food: {item} ({calories} kcal) on {date}") + +def log_activity(item: str, calories: int, date: str | None = None) -> None: + if date is None: + date = today() + with open("activities.csv", "a") as f: + f.write(f"{date},{item},{calories}\n") + print(f"Appended activity: {item} ({calories} kcal) on {date}") + + +def run_day_summary(date: str) -> None: + food: list[int] = [] + try: + with open("food.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + food.append(int(parts[2])) + except FileNotFoundError: + print("Could not read food.csv") + + activity: list[int] = [] + try: + with open("activities.csv") as f: + for line in f: + parts = line.strip().split(",") + if parts[0] == date: + activity.append(int(parts[2])) + except FileNotFoundError: + print("Could not read activities.csv") + + food_total = sum(food) + activity_total = sum(activity) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/05_use_dataclass.py b/2025/pythonic/05_use_dataclass.py new file mode 100644 index 00000000..435fa97d --- /dev/null +++ b/2025/pythonic/05_use_dataclass.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Iterator + +@dataclass +class Entry: + date: str + description: str + calories: int + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def append_entry(filename: str, entry: Entry) -> None: + with open(filename, "a") as f: + f.write(f"{entry.date},{entry.description},{entry.calories}\n") + print(f"Appended to {filename}: {entry.description} ({entry.calories} kcal)") + + +def read_entries(filename: str) -> Iterator[Entry]: + try: + with open(filename) as f: + for line in f: + date, desc, cals = line.strip().split(",") + yield Entry(date, desc, int(cals)) + except FileNotFoundError: + print(f"File not found: {filename}") + return iter([]) + +def run_day_summary(date: str) -> None: + food = list(read_entries("food.csv")) + activity = list(read_entries("activities.csv")) + + food_total = sum(entry.calories for entry in food if entry.date == date) + activity_total = sum(entry.calories for entry in activity if entry.date == date) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + + + +append_entry("food.csv", Entry(today(), "Banana", 100)) +append_entry("activities.csv", Entry(today(), "Running", 300)) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/06_use_pathlib.py b/2025/pythonic/06_use_pathlib.py new file mode 100644 index 00000000..ed06d526 --- /dev/null +++ b/2025/pythonic/06_use_pathlib.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Iterator + +FOOD_CSV = Path("food.csv") +ACTIVITY_CSV = Path("activities.csv") + +@dataclass +class Entry: + date: str + description: str + calories: int + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def append_entry(path: Path, entry: Entry) -> None: + with path.open("a") as f: + f.write(f"{entry.date},{entry.description},{entry.calories}\n") + print(f"Appended to {path}: {entry.description} ({entry.calories} kcal)") + +def read_entries(path: Path) -> Iterator[Entry]: + try: + with path.open() as f: + for line in f: + date, desc, cals = line.strip().split(",") + yield Entry(date, desc, int(cals)) + except FileNotFoundError: + print(f"File not found: {path}") + return iter([]) + +def run_day_summary(date: str) -> None: + food = list(read_entries(FOOD_CSV)) + activity = list(read_entries(ACTIVITY_CSV)) + + food_total = sum(entry.calories for entry in food if entry.date == date) + activity_total = sum(entry.calories for entry in activity if entry.date == date) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + +append_entry(FOOD_CSV, Entry(today(), "Banana", 100)) +append_entry(ACTIVITY_CSV, Entry(today(), "Running", 300)) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/07_add_helpers.py b/2025/pythonic/07_add_helpers.py new file mode 100644 index 00000000..39eb34b8 --- /dev/null +++ b/2025/pythonic/07_add_helpers.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Iterator + +FOOD_CSV = Path("food.csv") +ACTIVITY_CSV = Path("activities.csv") + +@dataclass +class Entry: + date: str + description: str + calories: int + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def append_entry(path: Path, entry: Entry) -> None: + with path.open("a") as f: + f.write(f"{entry.date},{entry.description},{entry.calories}\n") + print(f"Appended to {path}: {entry.description} ({entry.calories} kcal)") + +def log_food(description: str, calories: int, date: str | None = None): + entry = Entry(date or today(), description, calories) + append_entry(FOOD_CSV, entry) + +def log_activity(description: str, calories: int, date: str | None = None): + entry = Entry(date or today(), description, calories) + append_entry(ACTIVITY_CSV, entry) + +def read_entries(path: Path) -> Iterator[Entry]: + try: + with path.open() as f: + for line in f: + date, desc, cals = line.strip().split(",") + yield Entry(date, desc, int(cals)) + except FileNotFoundError: + print(f"File not found: {path}") + return iter([]) + +def run_day_summary(date: str) -> None: + food = list(read_entries(FOOD_CSV)) + activity = list(read_entries(ACTIVITY_CSV)) + + food_total = sum(entry.calories for entry in food if entry.date == date) + activity_total = sum(entry.calories for entry in activity if entry.date == date) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/08_add_logging.py b/2025/pythonic/08_add_logging.py new file mode 100644 index 00000000..354d169e --- /dev/null +++ b/2025/pythonic/08_add_logging.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +import logging +from typing import Iterator + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +FOOD_CSV = Path("food.csv") +ACTIVITY_CSV = Path("activities.csv") + +@dataclass +class Entry: + date: str + description: str + calories: int + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def append_entry(path: Path, entry: Entry) -> None: + with path.open("a") as f: + f.write(f"{entry.date},{entry.description},{entry.calories}\n") + logging.info(f"Appended to {path}: {entry.description} ({entry.calories} kcal)") + +def log_food(description: str, calories: int, date: str | None = None): + entry = Entry(date or today(), description, calories) + append_entry(FOOD_CSV, entry) + +def log_activity(description: str, calories: int, date: str | None = None): + entry = Entry(date or today(), description, calories) + append_entry(ACTIVITY_CSV, entry) + +def read_entries(path: Path) -> Iterator[Entry]: + try: + with path.open() as f: + for line in f: + date, desc, cals = line.strip().split(",") + yield Entry(date, desc, int(cals)) + except FileNotFoundError: + logging.warning(f"File not found: {path}") + return iter([]) + +def run_day_summary(date: str) -> None: + food = list(read_entries(FOOD_CSV)) + activity = list(read_entries(ACTIVITY_CSV)) + + food_total = sum(entry.calories for entry in food if entry.date == date) + activity_total = sum(entry.calories for entry in activity if entry.date == date) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + + logging.info(f"Daily summary: +{food_total} kcal intake, -{activity_total} burned, net = {net}") + +log_food("Banana", 100) +log_activity("Running", 300) +run_day_summary(today()) \ No newline at end of file diff --git a/2025/pythonic/10_final.py b/2025/pythonic/10_final.py new file mode 100644 index 00000000..94dbb7de --- /dev/null +++ b/2025/pythonic/10_final.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Iterator +from pathlib import Path +import logging + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +FOOD_CSV = Path("food.csv") +ACTIVITY_CSV = Path("activities.csv") + + +@dataclass +class Entry: + date: str + description: str + calories: int + +def today() -> str: + return datetime.now().strftime("%Y-%m-%d") + +def append_entry(path: Path, entry: Entry) -> None: + with path.open("a") as f: + f.write(f"{entry.date},{entry.description},{entry.calories}\n") + logging.info(f"Appended entry to {path}: {entry.description} ({entry.calories} kcal)") + +def log_food(description: str, calories: int, date: str | None = None) -> None: + if date is None: + date = today() + append_entry(FOOD_CSV, Entry(date, description, calories)) + +def log_activity(description: str, calories: int, date: str | None = None) -> None: + if date is None: + date = today() + append_entry(ACTIVITY_CSV, Entry(date, description, calories)) + +def read_entries(path: Path) -> Iterator[Entry]: + try: + with path.open() as f: + for line in f: + date, desc, cals = line.strip().split(",") + yield Entry(date, desc, int(cals)) + except FileNotFoundError: + logging.warning(f"File not found: {path}") + return iter([]) + + +def run_day_summary(date: str) -> None: + food = list(read_entries(FOOD_CSV)) + activity = list(read_entries(ACTIVITY_CSV)) + + food_total = sum(entry.calories for entry in food if entry.date == date) + activity_total = sum(entry.calories for entry in activity if entry.date == date) + net = food_total - activity_total + + print(f"\nSummary for {date}") + print(f" ๐ŸŽ Food: {food_total} kcal") + print(f" ๐Ÿƒ Activity: {activity_total} kcal") + print(f" โš–๏ธ Net: {net} kcal") + + logging.info(f"Daily summary: +{food_total} kcal intake, -{activity_total} burned, net = {net}") + +def main() -> None: + log_food("Banana", 100) + log_activity("Running", 300) + run_day_summary(today()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/pythonic/pyproject.toml b/2025/pythonic/pyproject.toml new file mode 100644 index 00000000..fd2dd2da --- /dev/null +++ b/2025/pythonic/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "pythonic" +version = "0.1.0" +description = "Example of refactoring to be more Pythonic" +requires-python = ">=3.14" +dependencies = [ +] From 87279cb3d4725553e3cd866d59ed301eb6a72efe Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 8 Oct 2025 16:47:27 +0200 Subject: [PATCH 053/113] Examples for learning Python fast --- 2025/learn/1_master_core.py | 16 +++++++ 2025/learn/2_pythonic.py | 12 +++++ 2025/learn/3_build_tools.py | 5 +++ 2025/learn/4_object_model.py | 10 +++++ 2025/learn/5_types_abstraction.py | 14 ++++++ 2025/learn/6_design.py | 42 +++++++++++++++++ 2025/learn/7_testing.py | 31 +++++++++++++ 2025/learn/8_internals.py | 20 +++++++++ 2025/learn/9_standard.py | 9 ++++ 2025/learn/pyproject.toml | 7 +++ 2025/learn/uv.lock | 75 +++++++++++++++++++++++++++++++ 11 files changed, 241 insertions(+) create mode 100644 2025/learn/1_master_core.py create mode 100644 2025/learn/2_pythonic.py create mode 100644 2025/learn/3_build_tools.py create mode 100644 2025/learn/4_object_model.py create mode 100644 2025/learn/5_types_abstraction.py create mode 100644 2025/learn/6_design.py create mode 100644 2025/learn/7_testing.py create mode 100644 2025/learn/8_internals.py create mode 100644 2025/learn/9_standard.py create mode 100644 2025/learn/pyproject.toml create mode 100644 2025/learn/uv.lock diff --git a/2025/learn/1_master_core.py b/2025/learn/1_master_core.py new file mode 100644 index 00000000..471a5f26 --- /dev/null +++ b/2025/learn/1_master_core.py @@ -0,0 +1,16 @@ +def main() -> None: + words = ["code", "python", "ai", "refactor", "bug"] + + length_map: dict[str, int] = {} + for word in words: + if len(word) > 4: + length_map[word] = len(word) + + print(length_map) + + # now with a dict comprehension + length_map_comp = {word: len(word) for word in words if len(word) > 4} + print(length_map_comp) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/2_pythonic.py b/2025/learn/2_pythonic.py new file mode 100644 index 00000000..0de4a7bb --- /dev/null +++ b/2025/learn/2_pythonic.py @@ -0,0 +1,12 @@ +def main() -> None: + names = ["Arjan", "Marieke", "Pim", "Sanne", "Daan", "Eva", "Lars"] + + for i in range(len(names)): + print(i, names[i]) + + # or more pythonic + for index, name in enumerate(names): + print(index, name) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/3_build_tools.py b/2025/learn/3_build_tools.py new file mode 100644 index 00000000..33b4f9cb --- /dev/null +++ b/2025/learn/3_build_tools.py @@ -0,0 +1,5 @@ +from pathlib import Path + +def rename_jpegs(folder: str): + for file in Path(folder).glob("*.jpeg"): + file.rename(file.with_suffix(".jpg")) \ No newline at end of file diff --git a/2025/learn/4_object_model.py b/2025/learn/4_object_model.py new file mode 100644 index 00000000..28b6c32b --- /dev/null +++ b/2025/learn/4_object_model.py @@ -0,0 +1,10 @@ +def greet(name: str) -> str: + return f"Hello, {name}!" + +def main() -> None: + say_hello = greet + + print(say_hello("Pythonista")) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/5_types_abstraction.py b/2025/learn/5_types_abstraction.py new file mode 100644 index 00000000..8e8aab09 --- /dev/null +++ b/2025/learn/5_types_abstraction.py @@ -0,0 +1,14 @@ +from typing import Protocol, Callable + +class Logger(Protocol): + def info(self, message: str) -> None: ... + def error(self, message: str) -> None: ... + +def process_order(order_id: int, logger: Logger) -> None: + logger.info(f"Processing order {order_id}") + + +ImageExporter = Callable[[bytes], None] + +def export_image(data: bytes, exporter: ImageExporter) -> None: + exporter(data) \ No newline at end of file diff --git a/2025/learn/6_design.py b/2025/learn/6_design.py new file mode 100644 index 00000000..fca4b15d --- /dev/null +++ b/2025/learn/6_design.py @@ -0,0 +1,42 @@ +from typing import Protocol + +def notify_user_no_di(user_email: str, message: str): + print(f"Sending email to {user_email}: {message}") + + +# Define a Protocol (interface) for notification services +class Notifier(Protocol): + def send(self, recipient: str, message: str) -> None: + ... + + +# Concrete implementation: sends email +class EmailNotifier: + def send(self, recipient: str, message: str) -> None: + print(f"[Email] To: {recipient} | Message: {message}") + + +# Concrete implementation: sends SMS +class SMSNotifier: + def send(self, recipient: str, message: str) -> None: + print(f"[SMS] To: {recipient} | Message: {message}") + + +# Business logic function with injected dependency +def notify_user(user: str, message: str, notifier: Notifier) -> None: + notifier.send(user, message) + + +def main() -> None: + email_notifier = EmailNotifier() + sms_notifier = SMSNotifier() + + # Send email + notify_user("alice@example.com", "Your invoice is ready.", email_notifier) + + # Send SMS + notify_user("+31612345678", "Your package is on the way!", sms_notifier) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/7_testing.py b/2025/learn/7_testing.py new file mode 100644 index 00000000..9d2183d0 --- /dev/null +++ b/2025/learn/7_testing.py @@ -0,0 +1,31 @@ +import pytest +import sys + +# Function we want to test +def is_prime(n: int) -> bool: + if n <= 1: + return False + for i in range(2, int(n**0.5) + 1): + if n % i == 0: + return False + return True + + +# Parametrized test: tests multiple inputs in one go +@pytest.mark.parametrize("n, expected", [ + (2, True), + (3, True), + (4, False), + (17, True), + (18, False), + (1, False), + (0, False), + (-5, False), +]) +def test_is_prime(n: int, expected: bool) -> None: + assert is_prime(n) == expected + + +# Optional: run pytest programmatically from the same file +if __name__ == "__main__": + sys.exit(pytest.main([__file__])) \ No newline at end of file diff --git a/2025/learn/8_internals.py b/2025/learn/8_internals.py new file mode 100644 index 00000000..219b2a2c --- /dev/null +++ b/2025/learn/8_internals.py @@ -0,0 +1,20 @@ +class Countdown: + def __init__(self, start: int): + self.current = start + + def __iter__(self): + return self + + def __next__(self): + if self.current <= 0: + raise StopIteration + self.current -= 1 + return self.current + 1 + +def main() -> None: + countdown = Countdown(5) + for number in countdown: + print(number) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/9_standard.py b/2025/learn/9_standard.py new file mode 100644 index 00000000..bcaa8f34 --- /dev/null +++ b/2025/learn/9_standard.py @@ -0,0 +1,9 @@ +import textwrap + + +def main() -> None: + text = "This is a sample text that will be wrapped to a specified width using the textwrap module in Python." + print(textwrap.fill(text, width=40)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/learn/pyproject.toml b/2025/learn/pyproject.toml new file mode 100644 index 00000000..974dda8c --- /dev/null +++ b/2025/learn/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "learning_python" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "pytest>=8.4.2", +] diff --git a/2025/learn/uv.lock b/2025/learn/uv.lock new file mode 100644 index 00000000..8a0fbe0b --- /dev/null +++ b/2025/learn/uv.lock @@ -0,0 +1,75 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "learning-python" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "pytest", specifier = ">=8.4.2" }] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] From 0ec8a7d242ffecb830c59e8fa1e94ce3ff9055e5 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 13 Nov 2025 22:38:13 +0100 Subject: [PATCH 054/113] Added aiGrunn code example --- 2025/aigrunn/1_adhoc.py | 90 ++++++++ 2025/aigrunn/2_chain.py | 101 +++++++++ 2025/aigrunn/3_observer.py | 170 +++++++++++++++ 2025/aigrunn/4_decl_travel.py | 29 +++ 2025/aigrunn/5_decl_job.py | 30 +++ 2025/aigrunn/ai_utils.py | 105 +++++++++ 2025/aigrunn/declarative_engine.py | 106 +++++++++ 2025/aigrunn/pyproject.toml | 9 + 2025/aigrunn/uv.lock | 340 +++++++++++++++++++++++++++++ 9 files changed, 980 insertions(+) create mode 100644 2025/aigrunn/1_adhoc.py create mode 100644 2025/aigrunn/2_chain.py create mode 100644 2025/aigrunn/3_observer.py create mode 100644 2025/aigrunn/4_decl_travel.py create mode 100644 2025/aigrunn/5_decl_job.py create mode 100644 2025/aigrunn/ai_utils.py create mode 100644 2025/aigrunn/declarative_engine.py create mode 100644 2025/aigrunn/pyproject.toml create mode 100644 2025/aigrunn/uv.lock diff --git a/2025/aigrunn/1_adhoc.py b/2025/aigrunn/1_adhoc.py new file mode 100644 index 00000000..35aa07d3 --- /dev/null +++ b/2025/aigrunn/1_adhoc.py @@ -0,0 +1,90 @@ +from typing import Any + +from pydantic import BaseModel + +from ai_utils import extract_structured_dict + + +# ------------------------------------------------------------------- +# Domain Pydantic Models +# ------------------------------------------------------------------- +class TravelInfo(BaseModel): + destination: str + days: int + hotel_needed: bool + car_rental: bool + + +class DestinationOnly(BaseModel): + destination: str + + +class DaysOnly(BaseModel): + days: int + + +class HotelOnly(BaseModel): + hotel_needed: bool + + +class CarOnly(BaseModel): + car_rental: bool + + +def book_trip_interactively() -> TravelInfo: + print("=== Ad-Hoc Travel Booking ===") + + info: dict[str, Any] = {} + + # ------------------------------ + # Step 1 โ€” Destination + # ------------------------------ + answer = input("Where are you traveling? ") + extracted = extract_structured_dict( + model_type=DestinationOnly, + user_answer=answer, + ) + info.update(extracted) + + # ------------------------------ + # Step 2 โ€” Days + # ------------------------------ + answer = input("How many days will you stay? ") + extracted = extract_structured_dict( + model_type=DaysOnly, + user_answer=answer, + ) + info.update(extracted) + + # ------------------------------ + # Step 3 โ€” Hotel needed + # ------------------------------ + answer = input("Do you need a hotel (yes/no)? ") + extracted = extract_structured_dict( + model_type=HotelOnly, + user_answer=answer, + ) + info.update(extracted) + + # ------------------------------ + # Step 4 โ€” Car rental + # ------------------------------ + answer = input("Do you need a rental car (yes/no)? ") + extracted = extract_structured_dict( + model_type=CarOnly, + user_answer=answer, + ) + info.update(extracted) + + # Build final validated model + return TravelInfo(**info) + + +def main() -> None: + result = book_trip_interactively() + print("\n=== Final Trip Info ===") + print(result.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/2025/aigrunn/2_chain.py b/2025/aigrunn/2_chain.py new file mode 100644 index 00000000..cc801c42 --- /dev/null +++ b/2025/aigrunn/2_chain.py @@ -0,0 +1,101 @@ +from typing import Any, Callable, Type + +from pydantic import BaseModel + +from ai_utils import extract_structured_dict + + +# ------------------------------------------------------------------- +# Domain Pydantic Models +# ------------------------------------------------------------------- +class TravelInfo(BaseModel): + destination: str + days: int + hotel_needed: bool + car_rental: bool + + +class DestinationOnly(BaseModel): + destination: str + + +class DaysOnly(BaseModel): + days: int + + +class HotelOnly(BaseModel): + hotel_needed: bool + + +class CarOnly(BaseModel): + car_rental: bool + + +# ------------------------------------------------------------------- +# Generic handler helper +# ------------------------------------------------------------------- +def run_step( + info: dict[str, Any], + prompt: str, + model_type: Type[BaseModel], +) -> dict[str, Any]: + """Generic LLM-powered structured extraction step.""" + answer = input(prompt) + extracted = extract_structured_dict( + model_type=model_type, + user_answer=answer, + ) + info.update(extracted) + return info + + +# ------------------------------------------------------------------- +# Step functions (Chain of Responsibility style) +# ------------------------------------------------------------------- +def destination_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step(info, "Where are you traveling? ", DestinationOnly) + + +def days_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step(info, "How many days will you stay? ", DaysOnly) + + +def hotel_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step(info, "Do you need a hotel (yes/no)? ", HotelOnly) + + +def car_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step(info, "Do you need a rental car (yes/no)? ", CarOnly) + + +# ------------------------------------------------------------------- +# Chain executor +# ------------------------------------------------------------------- +HandlerFn = Callable[[dict[str, Any]], dict[str, Any]] + + +def run_chain(*steps: HandlerFn) -> TravelInfo: + state: dict[str, Any] = {} + + for step in steps: + state = step(state) + + return TravelInfo(**state) + + +def main() -> None: + print("=== Chain of Responsibility (Using Generic Handler) ===\n") + + final_info = run_chain( + destination_step, + days_step, + hotel_step, + car_step, + ) + + print("\n=== Final Trip Info ===") + print(final_info.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/2025/aigrunn/3_observer.py b/2025/aigrunn/3_observer.py new file mode 100644 index 00000000..52fc9b05 --- /dev/null +++ b/2025/aigrunn/3_observer.py @@ -0,0 +1,170 @@ +from typing import Any, Callable, Type + +from pydantic import BaseModel + +from ai_utils import extract_structured_dict + + +# ------------------------------------------------------------------- +# Domain Pydantic Models +# ------------------------------------------------------------------- +class TravelInfo(BaseModel): + destination: str + days: int + hotel_needed: bool + car_rental: bool + + +class DestinationOnly(BaseModel): + destination: str + + +class DaysOnly(BaseModel): + days: int + + +class HotelOnly(BaseModel): + hotel_needed: bool + + +class CarOnly(BaseModel): + car_rental: bool + + +# ------------------------------------------------------------------- +# Observer Pattern (generic) +# ------------------------------------------------------------------- +ObserverFn = Callable[[str, Any], None] +_observers: list[ObserverFn] = [] + + +def add_observer(fn: ObserverFn) -> None: + """Register a callback that receives (event_name, payload).""" + _observers.append(fn) + + +def notify(event: str, payload: Any) -> None: + """Notify all observers about an event.""" + for obs in _observers: + obs(event, payload) + + +# ------------------------------------------------------------------- +# Generic Step Runner +# ------------------------------------------------------------------- +def run_step( + info: dict[str, Any], + prompt: str, + model_type: Type[BaseModel], + event_name: str, # NEW: Event for observers +) -> dict[str, Any]: + notify("before_step", {"step": event_name, "state": info}) + + user_input = input(prompt) + extracted = extract_structured_dict( + model_type=model_type, + user_answer=user_input, + ) + info.update(extracted) + + # Notify generic event + notify("after_field_extracted", {"step": event_name, "extracted": extracted}) + notify(f"{event_name}_completed", extracted) + + return info + + +# ------------------------------------------------------------------- +# Step Functions (Chain of Responsibility) +# ------------------------------------------------------------------- +def destination_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step( + info, + "Where are you traveling? ", + DestinationOnly, + event_name="destination", + ) + + +def days_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step( + info, + "How many days will you stay? ", + DaysOnly, + event_name="days", + ) + + +def hotel_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step( + info, + "Do you need a hotel (yes/no)? ", + HotelOnly, + event_name="hotel_needed", + ) + + +def car_step(info: dict[str, Any]) -> dict[str, Any]: + return run_step( + info, + "Do you need a rental car (yes/no)? ", + CarOnly, + event_name="car_rental", + ) + + +# ------------------------------------------------------------------- +# Chain Runner +# ------------------------------------------------------------------- +HandlerFn = Callable[[dict[str, Any]], dict[str, Any]] + + +def run_chain(*steps: HandlerFn) -> TravelInfo: + state: dict[str, Any] = {} + + notify("chain_started", state) + + for step in steps: + state = step(state) + + notify("chain_completed", state) + + return TravelInfo(**state) + + +# ------------------------------------------------------------------- +# Example Observer +# ------------------------------------------------------------------- +def logger(event: str, payload: Any) -> None: + print(f"[LOG] Event: {event} | Payload: {payload}") + + +def analytics(event: str, payload: Any) -> None: + # In a real system, you would send analytics here + if event.endswith("_completed"): + print(f"[ANALYTICS] Step '{event}' recorded.") + + +# ------------------------------------------------------------------- +# Main +# ------------------------------------------------------------------- +def main() -> None: + print("=== Chain of Responsibility + Observer Pattern ===\n") + + # Register observers + add_observer(logger) + add_observer(analytics) + + final_info = run_chain( + destination_step, + days_step, + hotel_step, + car_step, + ) + + print("\n=== Final Trip Info ===") + print(final_info.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/2025/aigrunn/4_decl_travel.py b/2025/aigrunn/4_decl_travel.py new file mode 100644 index 00000000..0481ca44 --- /dev/null +++ b/2025/aigrunn/4_decl_travel.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel + +from declarative_engine import run_declarative_flow + + +class TravelInfo(BaseModel): + destination: str + days: int + hotel_needed: bool + car_rental: bool + + +def main(): + instructions = """ + You are a travel booking assistant helping the user plan a trip. + """ + + result = run_declarative_flow( + model_type=TravelInfo, + domain_instructions=instructions, + debug=True, + ) + + print("\n=== Final Result ===") + print(result.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/2025/aigrunn/5_decl_job.py b/2025/aigrunn/5_decl_job.py new file mode 100644 index 00000000..9baf4257 --- /dev/null +++ b/2025/aigrunn/5_decl_job.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel + +from declarative_engine import run_declarative_flow + + +class ApplicantInfo(BaseModel): + full_name: str + years_experience: int + has_degree: bool + willing_to_relocate: bool + + +def main(): + instructions = """ + You are an Gronings dialect speaking HR assistant helping screen job applicants. + Collect the required information speaking Gronings dialect only. + """ + + result = run_declarative_flow( + model_type=ApplicantInfo, + domain_instructions=instructions, + debug=True, # Disable for production + ) + + print("\n=== Final Applicant Info ===") + print(result.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/2025/aigrunn/ai_utils.py b/2025/aigrunn/ai_utils.py new file mode 100644 index 00000000..e70fcd73 --- /dev/null +++ b/2025/aigrunn/ai_utils.py @@ -0,0 +1,105 @@ +import json +import os +from typing import Any, Type + +from dotenv import load_dotenv +from openai import OpenAI +from pydantic import BaseModel + +# ------------------------------------------------------------------- +# API key and LLM settings +# ------------------------------------------------------------------- +load_dotenv() +API_KEY = os.getenv("OPENAI_API_KEY") +if not API_KEY: + raise RuntimeError("Missing OPENAI_API_KEY in .env") + +client = OpenAI(api_key=API_KEY) + +LLM_MODEL = "gpt-5-mini" + + +# ------------------------------------------------------------------- +# Base wrapper โ€” ALL LLM CALLS go through this +# ------------------------------------------------------------------- +def call_llm( + *, + instructions: str, + context: str, +) -> str: + """ + Low-level unified LLM wrapper. + Sends instructions + context and returns raw assistant text. + """ + response = client.responses.create( + model=LLM_MODEL, + instructions=instructions, + input=context, + ) + return response.output_text.strip() + + +# ------------------------------------------------------------------- +# Extract JSON object from LLM text +# ------------------------------------------------------------------- +def extract_json_object(text: str) -> str: + text = text.strip() + if text.startswith("{") and text.endswith("}"): + return text + + start = text.find("{") + end = text.rfind("}") + if start != -1 and end != -1: + return text[start : end + 1] + + raise ValueError(f"LLM did not return JSON:\n{text}") + + +# ------------------------------------------------------------------- +# Universal structured extractor (built on call_llm) +# ------------------------------------------------------------------- +def extract_structured_dict( + *, + model_type: Type[BaseModel], + user_answer: str, + context: str | None = None, + allowed_fields: list[str] | None = None, +) -> dict[str, Any]: + """ + Universal structured extraction. Always returns a dict. + """ + + schema = model_type.model_json_schema() + + allowed = "" + if allowed_fields: + allowed_keys = ", ".join(f'"{f}"' for f in allowed_fields) + allowed = f"Valid JSON keys: {allowed_keys}\n" + + instructions = f""" +Respond strictly with a JSON object matching this schema: + +{schema} + +Rules: +- Extract ONLY fields explicitly stated by the user. +- Do NOT guess values. +- Do NOT output missing fields. +- {allowed} +- Convert yes/no into true/false when appropriate. +- Respond ONLY with valid JSON. No explanation or markdown. +""" + + ctx = ( + f"{context}\n\nUser answer: {user_answer}" + if context + else f"User answer: {user_answer}" + ) + + raw = call_llm( + instructions=instructions, + context=ctx, + ) + + json_text = extract_json_object(raw) + return json.loads(json_text) diff --git a/2025/aigrunn/declarative_engine.py b/2025/aigrunn/declarative_engine.py new file mode 100644 index 00000000..e7f04879 --- /dev/null +++ b/2025/aigrunn/declarative_engine.py @@ -0,0 +1,106 @@ +import json +from typing import Any, Type + +from pydantic import BaseModel + +from ai_utils import ( + call_llm, + extract_structured_dict, +) + + +def format_history(history: list[dict[str, str]]) -> str: + return "\n".join(f"{m['role'].upper()}: {m['content']}" for m in history) + + +def ask_next_question( + accumulated: dict[str, Any], + history: list[dict[str, str]], +) -> str: + instructions = ( + "Given the conversation history and partially filled data,\n" + "ask ONE question about a field that is still null.\n" + "Do NOT answer the question yourself.\n" + "Respond ONLY with the question." + ) + + context = ( + format_history(history) + + "\n\nCurrent field values:\n" + + json.dumps(accumulated) + ) + + return call_llm( + instructions=instructions, + context=context, + ) + + +def run_declarative_flow( + model_type: Type[BaseModel], + domain_instructions: str, + debug: bool = True, +) -> BaseModel: + print("=== Declarative LLM Data Collection ===\n") + + field_names = list(model_type.model_fields.keys()) + accumulated = {f: None for f in field_names} + + history = [ + { + "role": "system", + "content": ( + domain_instructions.strip() + "\n\n" + "You are collecting structured data.\n" + "Rules:\n" + "- Ask one question at a time.\n" + "- Only ask about missing fields.\n" + "- Never answer your own question.\n" + "- Never guess values.\n" + "- Stop when all fields are filled.\n" + ), + } + ] + + def log(*args): + if debug: + print("[DEBUG]", *args) + + def complete() -> bool: + return all(v is not None for v in accumulated.values()) + + # ------------------------------------------------------- + # Main loop + # ------------------------------------------------------- + while not complete(): + log("ACCUMULATED BEFORE:", accumulated) + + # 1. LLM chooses next field โ†’ question + question = ask_next_question( + accumulated=accumulated, + history=history, + ) + print(f"Assistant: {question}") + history.append({"role": "assistant", "content": question}) + + # 2. User answers + user_answer = input("You: ") + history.append({"role": "user", "content": user_answer}) + + # 3. Extract the information + partial = extract_structured_dict( + model_type=model_type, + user_answer=user_answer, + context=format_history(history), + allowed_fields=field_names, + ) + + # 4. Merge into accumulator + for k, v in partial.items(): + accumulated[k] = v + + log("ACCUMULATED AFTER:", accumulated) + print() + + # Validate final Pydantic model + return model_type(**accumulated) diff --git a/2025/aigrunn/pyproject.toml b/2025/aigrunn/pyproject.toml new file mode 100644 index 00000000..1cc601a8 --- /dev/null +++ b/2025/aigrunn/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "aigrunn" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "openai>=2.7.2", + "pydantic>=2.12.4", + "python-dotenv>=1.2.1", +] diff --git a/2025/aigrunn/uv.lock b/2025/aigrunn/uv.lock new file mode 100644 index 00000000..3a6777f5 --- /dev/null +++ b/2025/aigrunn/uv.lock @@ -0,0 +1,340 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aigrunn" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai", specifier = ">=2.7.2" }, + { name = "pydantic", specifier = ">=2.12.4" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "openai" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/e3/cec27fa28ef36c4ccea71e9e8c20be9b8539618732989a82027575aab9d4/openai-2.7.2.tar.gz", hash = "sha256:082ef61163074d8efad0035dd08934cf5e3afd37254f70fc9165dd6a8c67dcbd", size = 595732, upload-time = "2025-11-10T16:42:31.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/66/22cfe4b695b5fd042931b32c67d685e867bfd169ebf46036b95b57314c33/openai-2.7.2-py3-none-any.whl", hash = "sha256:116f522f4427f8a0a59b51655a356da85ce092f3ed6abeca65f03c8be6e073d9", size = 1008375, upload-time = "2025-11-10T16:42:28.574Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] From 9479108406e82d0032ec537a34cf0ed2316a87ea Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 30 Oct 2025 15:52:31 +0100 Subject: [PATCH 055/113] Added code example. --- 2025/lazy/01_eager_ui.py | 51 +++++++++++++++ 2025/lazy/02_lazy_ui_reload.py | 49 +++++++++++++++ 2025/lazy/03_generator_ui_reload.py | 49 +++++++++++++++ 2025/lazy/04_generator_ui_with_currency.py | 59 +++++++++++++++++ 2025/lazy/05_final.py | 73 ++++++++++++++++++++++ 2025/lazy/generate_sales_csv.py | 53 ++++++++++++++++ 2025/lazy/performance_tools.py | 21 +++++++ 2025/lazy/pyproject.toml | 7 +++ 2025/lazy/uv.lock | 34 ++++++++++ 9 files changed, 396 insertions(+) create mode 100644 2025/lazy/01_eager_ui.py create mode 100644 2025/lazy/02_lazy_ui_reload.py create mode 100644 2025/lazy/03_generator_ui_reload.py create mode 100644 2025/lazy/04_generator_ui_with_currency.py create mode 100644 2025/lazy/05_final.py create mode 100644 2025/lazy/generate_sales_csv.py create mode 100644 2025/lazy/performance_tools.py create mode 100644 2025/lazy/pyproject.toml create mode 100644 2025/lazy/uv.lock diff --git a/2025/lazy/01_eager_ui.py b/2025/lazy/01_eager_ui.py new file mode 100644 index 00000000..1809fe5b --- /dev/null +++ b/2025/lazy/01_eager_ui.py @@ -0,0 +1,51 @@ +import csv +from performance_tools import measure_performance + + +def load_sales(path: str) -> list[dict[str, str]]: + """Eagerly load the entire CSV into memory.""" + print("Loading CSV data...") + with open(path) as f: + reader = csv.DictReader(f) + return [row for row in reader] + + +@measure_performance +def analyse_sales(sales: list[dict[str, str]]) -> float: + total = 0.0 + for i, sale in enumerate(sales, start=1): + total += float(sale["amount"]) + return total + + +@measure_performance +def count_sales(sales: list[dict[str, str]]) -> int: + return len(sales) + + +def main() -> None: + sales = load_sales("sales.csv") # โŒ slow startup + + while True: + print("\nChoose an option:") + print("1. Analyse sales data") + print("2. Count total sales records") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + total = analyse_sales(sales) + print(f"Total sales: ${total:.2f}") + elif choice == "2": + count = count_sales(sales) + print(f"Number of sales: {count:,}") + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/lazy/02_lazy_ui_reload.py b/2025/lazy/02_lazy_ui_reload.py new file mode 100644 index 00000000..cc3be0a8 --- /dev/null +++ b/2025/lazy/02_lazy_ui_reload.py @@ -0,0 +1,49 @@ +import csv +from performance_tools import measure_performance + + +def load_sales(path: str) -> list[dict[str, str]]: + print("Loading CSV data lazily...") + with open(path) as f: + reader = csv.DictReader(f) + return [row for row in reader] + + +@measure_performance +def analyse_sales(path: str) -> float: + sales = load_sales(path) + return sum(float(s["amount"]) for s in sales) + + +@measure_performance +def count_sales(path: str) -> int: + sales = load_sales(path) + return len(sales) + + +def main() -> None: + path = "sales.csv" + + while True: + print("\nChoose an option:") + print("1. Analyse sales data") + print("2. Count total sales records") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + total = analyse_sales(path) + print(f"Total sales: ${total:.2f}") + elif choice == "2": + count = count_sales(path) + print(f"Number of sales: {count:,}") + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/lazy/03_generator_ui_reload.py b/2025/lazy/03_generator_ui_reload.py new file mode 100644 index 00000000..9a492b8b --- /dev/null +++ b/2025/lazy/03_generator_ui_reload.py @@ -0,0 +1,49 @@ +import csv +from typing import Generator +from performance_tools import measure_performance + + +def load_sales(path: str) -> Generator[dict[str, str], None, None]: + print("Streaming CSV data lazily...") + with open(path) as f: + reader = csv.DictReader(f) + for row in reader: + yield row + + +@measure_performance +def analyse_sales(path: str) -> float: + return sum(float(s["amount"]) for s in load_sales(path)) + + +@measure_performance +def count_sales(path: str) -> int: + return sum(1 for _ in load_sales(path)) + + +def main() -> None: + path = "sales.csv" + + while True: + print("\nChoose an option:") + print("1. Analyse sales data (streaming)") + print("2. Count total sales records (streaming)") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + total = analyse_sales(path) + print(f"Total sales: ${total:.2f}") + elif choice == "2": + count = count_sales(path) + print(f"Number of sales: {count:,}") + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/lazy/04_generator_ui_with_currency.py b/2025/lazy/04_generator_ui_with_currency.py new file mode 100644 index 00000000..e6491d84 --- /dev/null +++ b/2025/lazy/04_generator_ui_with_currency.py @@ -0,0 +1,59 @@ +import csv +import time +from typing import Generator +from performance_tools import measure_performance + + +def load_sales(path: str) -> Generator[dict[str, str], None, None]: + print("Streaming CSV data lazily...") + with open(path) as f: + reader = csv.DictReader(f) + for row in reader: + yield row + + +def get_conversion_rates() -> dict[str, float]: + print("Fetching conversion rates from remote service...") + time.sleep(2) + return {"USD": 1.0, "EUR": 1.1, "JPY": 0.007} + + +@measure_performance +def analyse_sales(path: str, currency: str) -> float: + total = sum(float(s["amount"]) for s in load_sales(path)) + rate = get_conversion_rates().get(currency, 1.0) + return total * rate + + +@measure_performance +def count_sales(path: str) -> int: + return sum(1 for _ in load_sales(path)) + + +def main() -> None: + path = "sales.csv" + + while True: + print("\nChoose an option:") + print("1. Analyse sales data") + print("2. Count total sales records") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + currency = input("Enter currency (USD/EUR/JPY): ").upper() or "USD" + converted_total = analyse_sales(path, currency) + print(f"Total sales in {currency}: {converted_total:.2f}") + elif choice == "2": + count = count_sales(path) + print(f"Number of sales: {count:,}") + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/lazy/05_final.py b/2025/lazy/05_final.py new file mode 100644 index 00000000..75d4b8d9 --- /dev/null +++ b/2025/lazy/05_final.py @@ -0,0 +1,73 @@ +import csv +import time +from functools import cache +from typing import Generator +from performance_tools import measure_performance + + +def load_sales(path: str) -> Generator[dict[str, str], None, None]: + print("Streaming CSV data lazily...") + with open(path) as f: + reader = csv.DictReader(f) + for row in reader: + yield row + + +@cache +def get_conversion_rates() -> dict[str, float]: + print("Fetching conversion rates from remote service...") + time.sleep(2) + return {"USD": 1.0, "EUR": 1.1, "JPY": 0.007} + + +@cache +def compute_total_sales(path: str) -> float: + print("Calculating total sales (this may take a while)...") + return sum(float(s["amount"]) for s in load_sales(path)) + + +@cache +def compute_sales_count(path: str) -> int: + print("Counting total sales records (this may take a while)...") + return sum(1 for _ in load_sales(path)) + + +@measure_performance +def analyse_sales(path: str, currency: str) -> float: + total = compute_total_sales(path) + rate = get_conversion_rates().get(currency, 1.0) + return total * rate + + +@measure_performance +def count_sales(path: str) -> int: + return compute_sales_count(path) + + +def main() -> None: + path = "sales.csv" + + while True: + print("\nChoose an option:") + print("1. Analyse sales data") + print("2. Count total sales records") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + currency = input("Enter currency (USD/EUR/JPY): ").upper() or "USD" + converted_total = analyse_sales(path, currency) + print(f"Total sales in {currency}: {converted_total:.2f}") + elif choice == "2": + count = count_sales(path) + print(f"Number of sales: {count:,}") + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/lazy/generate_sales_csv.py b/2025/lazy/generate_sales_csv.py new file mode 100644 index 00000000..f8f253c5 --- /dev/null +++ b/2025/lazy/generate_sales_csv.py @@ -0,0 +1,53 @@ +import csv +import random +from datetime import datetime, timedelta +from pathlib import Path + +# --- Configuration --- +OUTPUT_FILE = Path("sales.csv") +NUM_ROWS = 10_000_000 +PRINT_EVERY = 100_000 # progress update + +# --- Data generation helpers --- +def random_date(start: datetime, end: datetime) -> str: + """Return a random date between start and end as YYYY-MM-DD.""" + delta = end - start + random_days = random.randint(0, delta.days) + return (start + timedelta(days=random_days)).strftime("%Y-%m-%d") + +def random_amount() -> float: + """Return a random sales amount.""" + return round(random.uniform(5.0, 500.0), 2) + +def random_country() -> str: + """Return a random country.""" + return random.choice([ + "US", "DE", "NL", "FR", "JP", "IN", "BR", "GB", "CA", "AU" + ]) + +# --- Main generator --- +def generate_sales_csv(path: Path, num_rows: int) -> None: + """Generate a large CSV file with sales data.""" + start_date = datetime(2020, 1, 1) + end_date = datetime(2025, 1, 1) + + with path.open("w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["id", "date", "country", "product", "amount"]) + + for i in range(1, num_rows + 1): + writer.writerow([ + i, + random_date(start_date, end_date), + random_country(), + f"Product-{random.randint(1, 1000)}", + random_amount(), + ]) + if i % PRINT_EVERY == 0: + print(f"Wrote {i:,} rows...") + + print(f"โœ… Done! Wrote {num_rows:,} rows to {path.resolve()}") + +# --- Run --- +if __name__ == "__main__": + generate_sales_csv(OUTPUT_FILE, NUM_ROWS) \ No newline at end of file diff --git a/2025/lazy/performance_tools.py b/2025/lazy/performance_tools.py new file mode 100644 index 00000000..574d6d4a --- /dev/null +++ b/2025/lazy/performance_tools.py @@ -0,0 +1,21 @@ +import psutil +import os +from functools import wraps +import time + +def measure_performance(func): + """Measure execution time and memory usage.""" + @wraps(func) + def wrapper(*args, **kwargs): + process = psutil.Process(os.getpid()) + mem_before: float = process.memory_info().rss / 1024**2 + start: float = time.time() + + result = func(*args, **kwargs) + + duration: float = time.time() - start + mem_after: float = process.memory_info().rss / 1024**2 + print(f"โฑ {func.__name__} took {duration:.2f}s, " + f"used {mem_after - mem_before:.1f} MB\n") + return result + return wrapper \ No newline at end of file diff --git a/2025/lazy/pyproject.toml b/2025/lazy/pyproject.toml new file mode 100644 index 00000000..af2ef267 --- /dev/null +++ b/2025/lazy/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "lazy" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "psutil>=7.1.2", +] diff --git a/2025/lazy/uv.lock b/2025/lazy/uv.lock new file mode 100644 index 00000000..a795e3ea --- /dev/null +++ b/2025/lazy/uv.lock @@ -0,0 +1,34 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "lazy" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "psutil" }, +] + +[package.metadata] +requires-dist = [{ name = "psutil", specifier = ">=7.1.2" }] + +[[package]] +name = "psutil" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/ec/7b8e6b9b1d22708138630ef34c53ab2b61032c04f16adfdbb96791c8c70c/psutil-7.1.2.tar.gz", hash = "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018", size = 487424, upload-time = "2025-10-25T10:46:34.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9e/f1c5c746b4ed5320952acd3002d3962fe36f30524c00ea79fdf954cc6779/psutil-7.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc", size = 238640, upload-time = "2025-10-25T10:46:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/fd26216a735395cc25c3899634e34aeb41fb1f3dbb44acc67d9e594be562/psutil-7.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e", size = 239303, upload-time = "2025-10-25T10:46:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cd/7d96eaec4ef7742b845a9ce2759a2769ecce4ab7a99133da24abacbc9e41/psutil-7.1.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee", size = 281717, upload-time = "2025-10-25T10:46:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1a/7f0b84bdb067d35fe7fade5fff888408688caf989806ce2d6dae08c72dd5/psutil-7.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7", size = 284575, upload-time = "2025-10-25T10:47:00.944Z" }, + { url = "https://files.pythonhosted.org/packages/de/05/7820ef8f7b275268917e0c750eada5834581206d9024ca88edce93c4b762/psutil-7.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31", size = 249491, upload-time = "2025-10-25T10:47:03.174Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/58de399c7cb58489f08498459ff096cd76b3f1ddc4f224ec2c5ef729c7d0/psutil-7.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582", size = 244880, upload-time = "2025-10-25T10:47:05.228Z" }, + { url = "https://files.pythonhosted.org/packages/ae/89/b9f8d47ddbc52d7301fc868e8224e5f44ed3c7f55e6d0f54ecaf5dd9ff5e/psutil-7.1.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814", size = 237244, upload-time = "2025-10-25T10:47:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/8628c2f6b240680a67d73d8742bb9ff39b1820a693740e43096d5dcb01e5/psutil-7.1.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb", size = 238101, upload-time = "2025-10-25T10:47:09.523Z" }, + { url = "https://files.pythonhosted.org/packages/30/28/5e27f4d5a0e347f8e3cc16cd7d35533dbce086c95807f1f0e9cd77e26c10/psutil-7.1.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3", size = 258675, upload-time = "2025-10-25T10:47:11.082Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5c/79cf60c9acf36d087f0db0f82066fca4a780e97e5b3a2e4c38209c03d170/psutil-7.1.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a", size = 260203, upload-time = "2025-10-25T10:47:13.226Z" }, + { url = "https://files.pythonhosted.org/packages/f7/03/0a464404c51685dcb9329fdd660b1721e076ccd7b3d97dee066bcc9ffb15/psutil-7.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91", size = 246714, upload-time = "2025-10-25T10:47:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/6a/32/97ca2090f2f1b45b01b6aa7ae161cfe50671de097311975ca6eea3e7aabc/psutil-7.1.2-cp37-abi3-win_arm64.whl", hash = "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4", size = 243742, upload-time = "2025-10-25T10:47:17.302Z" }, +] From 107498943f4b5a69b76dec3b5617ea018456875b Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 11 Nov 2025 14:18:20 +0100 Subject: [PATCH 056/113] Added ttl example --- 2025/lazy/05_final.py | 48 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/2025/lazy/05_final.py b/2025/lazy/05_final.py index 75d4b8d9..3827a5af 100644 --- a/2025/lazy/05_final.py +++ b/2025/lazy/05_final.py @@ -1,11 +1,13 @@ import csv import time -from functools import cache -from typing import Generator +from functools import cache, wraps +from typing import Any, Callable, Generator + from performance_tools import measure_performance def load_sales(path: str) -> Generator[dict[str, str], None, None]: + """Stream sales data lazily from disk.""" print("Streaming CSV data lazily...") with open(path) as f: reader = csv.DictReader(f) @@ -13,27 +15,59 @@ def load_sales(path: str) -> Generator[dict[str, str], None, None]: yield row -@cache +# --- Custom TTL (Time-To-Live) cache decorator --- +def ttl_cache(seconds: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """A simple time-limited cache decorator.""" + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + cache_data = {} + cache_time = {} + + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, tuple(kwargs.items())) + current_time = time.time() + + # Check if cached and still valid + if key in cache_data and (current_time - cache_time[key]) < seconds: + return cache_data[key] + + # Otherwise, recompute and cache it + result = func(*args, **kwargs) + cache_data[key] = result + cache_time[key] = current_time + return result + + return wrapper + + return decorator + + +@ttl_cache(seconds=60) def get_conversion_rates() -> dict[str, float]: + """Simulate fetching conversion rates, cached for 60 seconds.""" print("Fetching conversion rates from remote service...") - time.sleep(2) + time.sleep(2) # Simulate API delay return {"USD": 1.0, "EUR": 1.1, "JPY": 0.007} @cache def compute_total_sales(path: str) -> float: + """Compute total sales and cache the numeric result.""" print("Calculating total sales (this may take a while)...") return sum(float(s["amount"]) for s in load_sales(path)) @cache def compute_sales_count(path: str) -> int: + """Compute and cache total record count.""" print("Counting total sales records (this may take a while)...") return sum(1 for _ in load_sales(path)) @measure_performance def analyse_sales(path: str, currency: str) -> float: + """Compute total converted sales and return result.""" total = compute_total_sales(path) rate = get_conversion_rates().get(currency, 1.0) return total * rate @@ -41,6 +75,7 @@ def analyse_sales(path: str, currency: str) -> float: @measure_performance def count_sales(path: str) -> int: + """Compute and return the total number of sales.""" return compute_sales_count(path) @@ -59,15 +94,18 @@ def main() -> None: currency = input("Enter currency (USD/EUR/JPY): ").upper() or "USD" converted_total = analyse_sales(path, currency) print(f"Total sales in {currency}: {converted_total:.2f}") + elif choice == "2": count = count_sales(path) print(f"Number of sales: {count:,}") + elif choice == "3": print("Goodbye!") break + else: print("Invalid choice, try again.") if __name__ == "__main__": - main() \ No newline at end of file + main() From 155c1d169f7716c9e347829ebc1d6c611a12738d Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 11 Nov 2025 14:19:00 +0100 Subject: [PATCH 057/113] Added threaded example --- 2025/lazy/{05_final.py => 05_ttl.py} | 0 2025/lazy/06_final.py | 127 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) rename 2025/lazy/{05_final.py => 05_ttl.py} (100%) create mode 100644 2025/lazy/06_final.py diff --git a/2025/lazy/05_final.py b/2025/lazy/05_ttl.py similarity index 100% rename from 2025/lazy/05_final.py rename to 2025/lazy/05_ttl.py diff --git a/2025/lazy/06_final.py b/2025/lazy/06_final.py new file mode 100644 index 00000000..3b69b2dc --- /dev/null +++ b/2025/lazy/06_final.py @@ -0,0 +1,127 @@ +import csv +import threading +import time +from functools import cache, wraps +from typing import Any, Callable, Generator + +from performance_tools import measure_performance + + +def load_sales(path: str) -> Generator[dict[str, str], None, None]: + """Stream sales data lazily from disk.""" + print("Streaming CSV data lazily...") + with open(path) as f: + reader = csv.DictReader(f) + for row in reader: + yield row + + +# --- Time-limited cache decorator --- +def ttl_cache(seconds: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """A simple time-limited cache decorator.""" + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + cache_data = {} + cache_time = {} + + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, tuple(kwargs.items())) + now = time.time() + + # Return cached result if still valid + if key in cache_data and (now - cache_time[key]) < seconds: + return cache_data[key] + + # Otherwise recompute and store + result = func(*args, **kwargs) + cache_data[key] = result + cache_time[key] = now + return result + + return wrapper + + return decorator + + +@ttl_cache(seconds=60) +def get_conversion_rates() -> dict[str, float]: + """Simulate fetching conversion rates, cached for 60 seconds.""" + print("Fetching conversion rates from remote service...") + time.sleep(2) + return {"USD": 1.0, "EUR": 1.1, "JPY": 0.007} + + +@cache +def compute_total_sales(path: str) -> float: + """Compute total sales and cache the numeric result.""" + print("Calculating total sales (this may take a while)...") + return sum(float(s["amount"]) for s in load_sales(path)) + + +@cache +def compute_sales_count(path: str) -> int: + """Compute and cache total record count.""" + print("Counting total sales records (this may take a while)...") + return sum(1 for _ in load_sales(path)) + + +@measure_performance +def analyse_sales(path: str, currency: str) -> float: + """Compute total converted sales and return result.""" + total = compute_total_sales(path) + rate = get_conversion_rates().get(currency, 1.0) + return total * rate + + +@measure_performance +def count_sales(path: str) -> int: + """Compute and return the total number of sales.""" + return compute_sales_count(path) + + +def preload_sales_data(path: str) -> None: + """Preload sales data in a background thread.""" + + def _preload(): + print("\n[Background] Preloading sales data...") + compute_total_sales(path) + compute_sales_count(path) + print("[Background] Sales data preloaded and cached.") + + threading.Thread(target=_preload, daemon=True).start() + + +def main() -> None: + path = "sales.csv" + + # Show UI immediately โ€” preload in background + preload_sales_data(path) + + while True: + print("\nChoose an option:") + print("1. Analyse sales data") + print("2. Count total sales records") + print("3. Quit") + + choice = input("> ") + + if choice == "1": + currency = input("Enter currency (USD/EUR/JPY): ").upper() or "USD" + converted_total = analyse_sales(path, currency) + print(f"Total sales in {currency}: {converted_total:.2f}") + + elif choice == "2": + count = count_sales(path) + print(f"Number of sales: {count:,}") + + elif choice == "3": + print("Goodbye!") + break + + else: + print("Invalid choice, try again.") + + +if __name__ == "__main__": + main() From 7192b8931271f08e1418d639925e4eb37917df0e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 29 Oct 2025 16:14:38 +0100 Subject: [PATCH 058/113] Rough code example. --- 2025/production/00_starting_point.py | 8 + 2025/production/01_validation.py | 11 + 2025/production/02_logging.py | 18 ++ 2025/production/03_service.py | 15 + 2025/production/04_config.py | 12 + 2025/production/05_security.py | 14 + 2025/production/06_health_check.py | 3 + 2025/production/database.py | 5 + 2025/production/deploy_example.yaml | 24 ++ 2025/production/models.py | 15 + 2025/production/pyproject.toml | 12 + 2025/production/uv.lock | 408 +++++++++++++++++++++++++++ 12 files changed, 545 insertions(+) create mode 100644 2025/production/00_starting_point.py create mode 100644 2025/production/01_validation.py create mode 100644 2025/production/02_logging.py create mode 100644 2025/production/03_service.py create mode 100644 2025/production/04_config.py create mode 100644 2025/production/05_security.py create mode 100644 2025/production/06_health_check.py create mode 100644 2025/production/database.py create mode 100644 2025/production/deploy_example.yaml create mode 100644 2025/production/models.py create mode 100644 2025/production/pyproject.toml create mode 100644 2025/production/uv.lock diff --git a/2025/production/00_starting_point.py b/2025/production/00_starting_point.py new file mode 100644 index 00000000..361059c7 --- /dev/null +++ b/2025/production/00_starting_point.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/convert") +def convert(from_currency: str, to_currency: str, amount: float): + rate = 1.1 # hardcoded + return {"result": amount * rate} \ No newline at end of file diff --git a/2025/production/01_validation.py b/2025/production/01_validation.py new file mode 100644 index 00000000..d6df66c7 --- /dev/null +++ b/2025/production/01_validation.py @@ -0,0 +1,11 @@ +from pydantic import condecimal +from fastapi import Query + +@app.get("/convert") +def convert( + from_currency: str = Query(..., min_length=3, max_length=3), + to_currency: str = Query(..., min_length=3, max_length=3), + amount: condecimal(gt=0) = Query(...) +): + rate = 1.1 + return {"result": float(amount * rate)} \ No newline at end of file diff --git a/2025/production/02_logging.py b/2025/production/02_logging.py new file mode 100644 index 00000000..ab5ad806 --- /dev/null +++ b/2025/production/02_logging.py @@ -0,0 +1,18 @@ +import logging +from fastapi import HTTPException, Request +import sentry_sdk + +logging.basicConfig(level=logging.INFO) +sentry_sdk.init(dsn="your-sentry-dsn") # from config + +@app.get("/convert") +def convert(..., request: Request): + try: + rate = 1.1 + result = float(amount * rate) + logging.info(f"Converted {amount} {from_currency} โ†’ {to_currency}") + return {"result": result} + except Exception as e: + logging.error(f"Conversion failed: {e}") + sentry_sdk.capture_exception(e) + raise HTTPException(status_code=500, detail="Internal error") \ No newline at end of file diff --git a/2025/production/03_service.py b/2025/production/03_service.py new file mode 100644 index 00000000..78c4548c --- /dev/null +++ b/2025/production/03_service.py @@ -0,0 +1,15 @@ +import httpx + +class ExchangeRateService: + def __init__(self, api_url: str): + self.api_url = api_url + + def get_rate(self, from_currency: str, to_currency: str) -> float: + url = f"{self.api_url}?base={from_currency}&symbols={to_currency}" + response = httpx.get(url, timeout=5.0) + response.raise_for_status() + data = response.json() + rate = data["rates"].get(to_currency) + if not rate or rate <= 0: + raise ValueError("Invalid exchange rate") + return rate \ No newline at end of file diff --git a/2025/production/04_config.py b/2025/production/04_config.py new file mode 100644 index 00000000..ab303812 --- /dev/null +++ b/2025/production/04_config.py @@ -0,0 +1,12 @@ +# config.py +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + api_url: str = "https://api.exchangerate.host/latest" + sentry_dsn: str = "" + log_level: str = "INFO" + + class Config: + env_file = ".env" + +settings = Settings() \ No newline at end of file diff --git a/2025/production/05_security.py b/2025/production/05_security.py new file mode 100644 index 00000000..341e1ad0 --- /dev/null +++ b/2025/production/05_security.py @@ -0,0 +1,14 @@ +from slowapi import Limiter +from slowapi.util import get_remote_address +from fastapi import Depends + +limiter = Limiter(key_func=get_remote_address) +app.state.limiter = limiter + +def get_service(): + return rate_service + +@app.get("/convert") +@limiter.limit("5/minute") +def convert(..., service: ExchangeRateService = Depends(get_service)): + ... \ No newline at end of file diff --git a/2025/production/06_health_check.py b/2025/production/06_health_check.py new file mode 100644 index 00000000..19b52242 --- /dev/null +++ b/2025/production/06_health_check.py @@ -0,0 +1,3 @@ +@app.get("/health") +def health(): + return {"status": "ok"} \ No newline at end of file diff --git a/2025/production/database.py b/2025/production/database.py new file mode 100644 index 00000000..71bc6528 --- /dev/null +++ b/2025/production/database.py @@ -0,0 +1,5 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +engine = create_engine("sqlite:///./db.sqlite3", connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) \ No newline at end of file diff --git a/2025/production/deploy_example.yaml b/2025/production/deploy_example.yaml new file mode 100644 index 00000000..81a8f583 --- /dev/null +++ b/2025/production/deploy_example.yaml @@ -0,0 +1,24 @@ +name: CI/CD + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + - run: pip install poetry + - run: poetry install + - run: poetry run pytest + + docker: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: docker build -t myapp . \ No newline at end of file diff --git a/2025/production/models.py b/2025/production/models.py new file mode 100644 index 00000000..5f2698c8 --- /dev/null +++ b/2025/production/models.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, String, DateTime +import datetime + +Base = declarative_base() + +class Conversion(Base): + __tablename__ = "conversions" + + id = Column(Integer, primary_key=True) + from_currency = Column(String) + to_currency = Column(String) + amount = Column(Integer) + result = Column(Integer) + timestamp = Column(DateTime, default=datetime.datetime.utcnow) \ No newline at end of file diff --git a/2025/production/pyproject.toml b/2025/production/pyproject.toml new file mode 100644 index 00000000..5e382056 --- /dev/null +++ b/2025/production/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "production" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "alembic>=1.17.1", + "fastapi>=0.120.2", + "httpx>=0.28.1", + "slowapi>=0.1.9", + "sqlalchemy>=2.0.44", + "uvicorn>=0.38.0", +] diff --git a/2025/production/uv.lock b/2025/production/uv.lock new file mode 100644 index 00000000..e0f454e9 --- /dev/null +++ b/2025/production/uv.lock @@ -0,0 +1,408 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "alembic" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/b6/2a81d7724c0c124edc5ec7a167e85858b6fd31b9611c6fb8ecf617b7e2d3/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", size = 1981285, upload-time = "2025-10-29T00:23:16.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848, upload-time = "2025-10-29T00:23:18.79Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[package]] +name = "fastapi" +version = "0.120.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/fb/79e556bc8f9d360e5cc2fa7364a7ad6bda6f1736938b43a2791fa8baee7b/fastapi-0.120.2.tar.gz", hash = "sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac", size = 338684, upload-time = "2025-10-29T13:47:35.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/cc/1c33d05f62c9349bb80dfe789cc9a7409bdfb337a63fa347fd651d25294a/fastapi-0.120.2-py3-none-any.whl", hash = "sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb", size = 108383, upload-time = "2025-10-29T13:47:32.978Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "limits" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/e5/c968d43a65128cd54fb685f257aafb90cd5e4e1c67d084a58f0e4cbed557/limits-5.6.0.tar.gz", hash = "sha256:807fac75755e73912e894fdd61e2838de574c5721876a19f7ab454ae1fffb4b5", size = 182984, upload-time = "2025-09-29T17:15:22.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/96/4fcd44aed47b8fcc457653b12915fcad192cd646510ef3f29fd216f4b0ab/limits-5.6.0-py3-none-any.whl", hash = "sha256:b585c2104274528536a5b68864ec3835602b3c4a802cd6aa0b07419798394021", size = 60604, upload-time = "2025-09-29T17:15:18.419Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "production" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "alembic" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "slowapi" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.17.1" }, + { name = "fastapi", specifier = ">=0.120.2" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "slowapi", specifier = ">=0.1.9" }, + { name = "sqlalchemy", specifier = ">=2.0.44" }, + { name = "uvicorn", specifier = ">=0.38.0" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] From dd121a57a754e5677c04b46b6a87a9b8d4f7568f Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 30 Oct 2025 13:31:47 +0100 Subject: [PATCH 059/113] More work on production code example --- 2025/production/06_health_check.py | 3 - ..._starting_point.py => 0_starting_point.py} | 0 .../{01_validation.py => 1_validation.py} | 0 2025/production/2_testing.py | 9 + .../{02_logging.py => 3_logging.py} | 14 +- .../{03_service.py => 4_service.py} | 0 2025/production/{04_config.py => 5_config.py} | 0 .../{05_security.py => 6_security.py} | 0 2025/production/exchange_app/.env.example | 4 + 2025/production/exchange_app/api.py | 30 ++ 2025/production/exchange_app/config.py | 14 + .../production/{ => exchange_app}/database.py | 0 2025/production/exchange_app/main.py | 16 + 2025/production/exchange_app/models.py | 24 + 2025/production/exchange_app/pyproject.toml | 13 + 2025/production/exchange_app/seed_rates.py | 23 + 2025/production/exchange_app/services.py | 39 ++ .../production/exchange_app/tests/conftest.py | 38 ++ .../production/exchange_app/tests/test_api.py | 28 ++ .../exchange_app/tests/test_service.py | 21 + 2025/production/exchange_app/uv.lock | 457 ++++++++++++++++++ 2025/production/models.py | 15 - 22 files changed, 725 insertions(+), 23 deletions(-) delete mode 100644 2025/production/06_health_check.py rename 2025/production/{00_starting_point.py => 0_starting_point.py} (100%) rename 2025/production/{01_validation.py => 1_validation.py} (100%) create mode 100644 2025/production/2_testing.py rename 2025/production/{02_logging.py => 3_logging.py} (70%) rename 2025/production/{03_service.py => 4_service.py} (100%) rename 2025/production/{04_config.py => 5_config.py} (100%) rename 2025/production/{05_security.py => 6_security.py} (100%) create mode 100644 2025/production/exchange_app/.env.example create mode 100644 2025/production/exchange_app/api.py create mode 100644 2025/production/exchange_app/config.py rename 2025/production/{ => exchange_app}/database.py (100%) create mode 100644 2025/production/exchange_app/main.py create mode 100644 2025/production/exchange_app/models.py create mode 100644 2025/production/exchange_app/pyproject.toml create mode 100644 2025/production/exchange_app/seed_rates.py create mode 100644 2025/production/exchange_app/services.py create mode 100644 2025/production/exchange_app/tests/conftest.py create mode 100644 2025/production/exchange_app/tests/test_api.py create mode 100644 2025/production/exchange_app/tests/test_service.py create mode 100644 2025/production/exchange_app/uv.lock delete mode 100644 2025/production/models.py diff --git a/2025/production/06_health_check.py b/2025/production/06_health_check.py deleted file mode 100644 index 19b52242..00000000 --- a/2025/production/06_health_check.py +++ /dev/null @@ -1,3 +0,0 @@ -@app.get("/health") -def health(): - return {"status": "ok"} \ No newline at end of file diff --git a/2025/production/00_starting_point.py b/2025/production/0_starting_point.py similarity index 100% rename from 2025/production/00_starting_point.py rename to 2025/production/0_starting_point.py diff --git a/2025/production/01_validation.py b/2025/production/1_validation.py similarity index 100% rename from 2025/production/01_validation.py rename to 2025/production/1_validation.py diff --git a/2025/production/2_testing.py b/2025/production/2_testing.py new file mode 100644 index 00000000..68aa3c6a --- /dev/null +++ b/2025/production/2_testing.py @@ -0,0 +1,9 @@ +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_convert(): + res = client.get("/convert?from_currency=USD&to_currency=EUR&amount=100") + assert res.status_code == 200 + assert "result" in res.json() \ No newline at end of file diff --git a/2025/production/02_logging.py b/2025/production/3_logging.py similarity index 70% rename from 2025/production/02_logging.py rename to 2025/production/3_logging.py index ab5ad806..bee5613d 100644 --- a/2025/production/02_logging.py +++ b/2025/production/3_logging.py @@ -1,18 +1,22 @@ import logging -from fastapi import HTTPException, Request import sentry_sdk +from fastapi import HTTPException, Request -logging.basicConfig(level=logging.INFO) -sentry_sdk.init(dsn="your-sentry-dsn") # from config +sentry_sdk.init(dsn=settings.sentry_dsn) +logging.basicConfig(level=settings.log_level) @app.get("/convert") def convert(..., request: Request): try: - rate = 1.1 + rate = 1.1 # or from a service result = float(amount * rate) logging.info(f"Converted {amount} {from_currency} โ†’ {to_currency}") return {"result": result} except Exception as e: logging.error(f"Conversion failed: {e}") sentry_sdk.capture_exception(e) - raise HTTPException(status_code=500, detail="Internal error") \ No newline at end of file + raise HTTPException(status_code=500, detail="Internal error") + +@app.get("/health") +def health(): + return {"status": "ok"} \ No newline at end of file diff --git a/2025/production/03_service.py b/2025/production/4_service.py similarity index 100% rename from 2025/production/03_service.py rename to 2025/production/4_service.py diff --git a/2025/production/04_config.py b/2025/production/5_config.py similarity index 100% rename from 2025/production/04_config.py rename to 2025/production/5_config.py diff --git a/2025/production/05_security.py b/2025/production/6_security.py similarity index 100% rename from 2025/production/05_security.py rename to 2025/production/6_security.py diff --git a/2025/production/exchange_app/.env.example b/2025/production/exchange_app/.env.example new file mode 100644 index 00000000..176e9275 --- /dev/null +++ b/2025/production/exchange_app/.env.example @@ -0,0 +1,4 @@ +api_url=https://api.exchangerate.host/latest +log_level=INFO +sentry_dsn= +database_url=sqlite:///./db.sqlite3 \ No newline at end of file diff --git a/2025/production/exchange_app/api.py b/2025/production/exchange_app/api.py new file mode 100644 index 00000000..c75bb188 --- /dev/null +++ b/2025/production/exchange_app/api.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from pydantic import condecimal +from sqlalchemy.orm import Session +from .database import get_db +from .services import ExchangeRateService +from slowapi import Limiter +from slowapi.util import get_remote_address + +router = APIRouter() +limiter = Limiter(key_func=get_remote_address) + +@router.get("/convert") +@limiter.limit("5/minute") +def convert( + request: Request, + from_currency: str = Query(..., min_length=3, max_length=3), + to_currency: str = Query(..., min_length=3, max_length=3), + amount: condecimal(gt=0) = Query(...), + db: Session = Depends(get_db), +): + try: + service = ExchangeRateService(db) + return service.convert(from_currency, to_currency, float(amount)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/health") +def health(): + return {"status": "ok"} \ No newline at end of file diff --git a/2025/production/exchange_app/config.py b/2025/production/exchange_app/config.py new file mode 100644 index 00000000..079c3753 --- /dev/null +++ b/2025/production/exchange_app/config.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + +class Settings(BaseSettings): + sentry_dsn: str = "" + log_level: str = "INFO" + database_url: str = "sqlite:///./db.sqlite3" + + class Config: + env_file = ".env" + +@lru_cache +def get_settings(): + return Settings() \ No newline at end of file diff --git a/2025/production/database.py b/2025/production/exchange_app/database.py similarity index 100% rename from 2025/production/database.py rename to 2025/production/exchange_app/database.py diff --git a/2025/production/exchange_app/main.py b/2025/production/exchange_app/main.py new file mode 100644 index 00000000..2419f9a2 --- /dev/null +++ b/2025/production/exchange_app/main.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from .api import router, limiter +from .models import Base +from .database import engine +from .config import settings +import logging +import sentry_sdk + +Base.metadata.create_all(bind=engine) + +sentry_sdk.init(dsn=settings.sentry_dsn) +logging.basicConfig(level=settings.log_level) + +app = FastAPI() +app.include_router(router) +app.state.limiter = limiter \ No newline at end of file diff --git a/2025/production/exchange_app/models.py b/2025/production/exchange_app/models.py new file mode 100644 index 00000000..0db4b703 --- /dev/null +++ b/2025/production/exchange_app/models.py @@ -0,0 +1,24 @@ +from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, String, DateTime, Float +import datetime + +Base = declarative_base() + +class Conversion(Base): + __tablename__ = "conversions" + + id = Column(Integer, primary_key=True) + from_currency = Column(String(3)) + to_currency = Column(String(3)) + amount = Column(Float) + result = Column(Float) + timestamp = Column(DateTime, default=datetime.datetime.utcnow) + +class ConversionRate(Base): + __tablename__ = "conversion_rates" + + id = Column(Integer, primary_key=True) + from_currency = Column(String(3)) + to_currency = Column(String(3)) + rate = Column(Float) + timestamp = Column(DateTime, default=datetime.datetime.utcnow) \ No newline at end of file diff --git a/2025/production/exchange_app/pyproject.toml b/2025/production/exchange_app/pyproject.toml new file mode 100644 index 00000000..74ece992 --- /dev/null +++ b/2025/production/exchange_app/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "exchange_app" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "alembic>=1.17.1", + "fastapi>=0.120.2", + "httpx>=0.28.1", + "pytest>=8.4.2", + "slowapi>=0.1.9", + "sqlalchemy>=2.0.44", + "uvicorn>=0.38.0", +] diff --git a/2025/production/exchange_app/seed_rates.py b/2025/production/exchange_app/seed_rates.py new file mode 100644 index 00000000..a7856ff8 --- /dev/null +++ b/2025/production/exchange_app/seed_rates.py @@ -0,0 +1,23 @@ +from sqlalchemy.orm import Session +from .models import ConversionRate +from .database import SessionLocal + +sample_rates = [ + {"from_currency": "USD", "to_currency": "EUR", "rate": 0.91}, + {"from_currency": "EUR", "to_currency": "USD", "rate": 1.10}, + {"from_currency": "USD", "to_currency": "JPY", "rate": 150.0}, + {"from_currency": "GBP", "to_currency": "USD", "rate": 1.28}, + {"from_currency": "USD", "to_currency": "GBP", "rate": 0.78}, +] + +def main(): + db: Session = SessionLocal() + for entry in sample_rates: + rate = ConversionRate(**entry) + db.add(rate) + db.commit() + db.close() + print("Seeded exchange rates.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2025/production/exchange_app/services.py b/2025/production/exchange_app/services.py new file mode 100644 index 00000000..88d115c1 --- /dev/null +++ b/2025/production/exchange_app/services.py @@ -0,0 +1,39 @@ +from sqlalchemy.orm import Session +from .models import ConversionRate, Conversion +from fastapi import HTTPException +import datetime + +class ExchangeRateService: + def __init__(self, db: Session): + self.db = db + + def convert(self, from_currency: str, to_currency: str, amount: float) -> dict: + from_currency = from_currency.upper() + to_currency = to_currency.upper() + + rate_entry = ( + self.db.query(ConversionRate) + .filter_by(from_currency=from_currency, to_currency=to_currency) + .order_by(ConversionRate.timestamp.desc()) + .first() + ) + + if not rate_entry or rate_entry.rate <= 0: + raise HTTPException(status_code=404, detail="Exchange rate not found") + + result = amount * rate_entry.rate + + conversion = Conversion( + from_currency=from_currency, + to_currency=to_currency, + amount=amount, + result=result, + timestamp=datetime.datetime.utcnow() + ) + self.db.add(conversion) + self.db.commit() + + return { + "rate": rate_entry.rate, + "result": result + } \ No newline at end of file diff --git a/2025/production/exchange_app/tests/conftest.py b/2025/production/exchange_app/tests/conftest.py new file mode 100644 index 00000000..7c22a018 --- /dev/null +++ b/2025/production/exchange_app/tests/conftest.py @@ -0,0 +1,38 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from exchange_app.models import Base +from exchange_app.main import app +from exchange_app.database import get_db + +# Use in-memory SQLite for tests +TEST_DATABASE_URL = "sqlite:///:memory:" + +engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False}) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Override the get_db dependency +@pytest.fixture(autouse=True) +def override_get_db(): + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + +app.dependency_overrides[get_db] = override_get_db + +@pytest.fixture +def client(): + return TestClient(app) + +@pytest.fixture +def db_session(): + db = TestingSessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/2025/production/exchange_app/tests/test_api.py b/2025/production/exchange_app/tests/test_api.py new file mode 100644 index 00000000..2b49e5a3 --- /dev/null +++ b/2025/production/exchange_app/tests/test_api.py @@ -0,0 +1,28 @@ +from ..models import ConversionRate + +def test_convert_success(client, db_session): + # Arrange: Seed rate + db_session.add(ConversionRate( + from_currency="USD", + to_currency="EUR", + rate=0.9 + )) + db_session.commit() + + # Act + response = client.get("/convert?from_currency=USD&to_currency=EUR&amount=100") + + # Assert + assert response.status_code == 200 + data = response.json() + assert data["rate"] == 0.9 + assert data["result"] == 90.0 + + +def test_convert_missing_rate(client): + # Act + response = client.get("/convert?from_currency=GBP&to_currency=JPY&amount=50") + + # Assert + assert response.status_code == 500 # Internal server error from missing rate + assert "Exchange rate not found" in response.json()["detail"] \ No newline at end of file diff --git a/2025/production/exchange_app/tests/test_service.py b/2025/production/exchange_app/tests/test_service.py new file mode 100644 index 00000000..ed2593c5 --- /dev/null +++ b/2025/production/exchange_app/tests/test_service.py @@ -0,0 +1,21 @@ +from exchange_app.models import ConversionRate +from exchange_app.services import ExchangeRateService + +def test_service_convert_valid(db_session): + db_session.add(ConversionRate(from_currency="USD", to_currency="JPY", rate=150)) + db_session.commit() + + service = ExchangeRateService(db_session) + result = service.convert("usd", "jpy", 10) + + assert result["rate"] == 150 + assert result["result"] == 1500 + + +def test_service_convert_invalid_currency(db_session): + service = ExchangeRateService(db_session) + try: + service.convert("AAA", "BBB", 10) + assert False, "Expected HTTPException" + except Exception as e: + assert "Exchange rate not found" in str(e) \ No newline at end of file diff --git a/2025/production/exchange_app/uv.lock b/2025/production/exchange_app/uv.lock new file mode 100644 index 00000000..01b782e7 --- /dev/null +++ b/2025/production/exchange_app/uv.lock @@ -0,0 +1,457 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "alembic" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/b6/2a81d7724c0c124edc5ec7a167e85858b6fd31b9611c6fb8ecf617b7e2d3/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", size = 1981285, upload-time = "2025-10-29T00:23:16.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848, upload-time = "2025-10-29T00:23:18.79Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "exchange-app" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "alembic" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "slowapi" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.17.1" }, + { name = "fastapi", specifier = ">=0.120.2" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "slowapi", specifier = ">=0.1.9" }, + { name = "sqlalchemy", specifier = ">=2.0.44" }, + { name = "uvicorn", specifier = ">=0.38.0" }, +] + +[[package]] +name = "fastapi" +version = "0.120.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/fb/79e556bc8f9d360e5cc2fa7364a7ad6bda6f1736938b43a2791fa8baee7b/fastapi-0.120.2.tar.gz", hash = "sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac", size = 338684, upload-time = "2025-10-29T13:47:35.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/cc/1c33d05f62c9349bb80dfe789cc9a7409bdfb337a63fa347fd651d25294a/fastapi-0.120.2-py3-none-any.whl", hash = "sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb", size = 108383, upload-time = "2025-10-29T13:47:32.978Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "limits" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/e5/c968d43a65128cd54fb685f257aafb90cd5e4e1c67d084a58f0e4cbed557/limits-5.6.0.tar.gz", hash = "sha256:807fac75755e73912e894fdd61e2838de574c5721876a19f7ab454ae1fffb4b5", size = 182984, upload-time = "2025-09-29T17:15:22.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/96/4fcd44aed47b8fcc457653b12915fcad192cd646510ef3f29fd216f4b0ab/limits-5.6.0-py3-none-any.whl", hash = "sha256:b585c2104274528536a5b68864ec3835602b3c4a802cd6aa0b07419798394021", size = 60604, upload-time = "2025-09-29T17:15:18.419Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/19/5e5bcd855d808892fe02d49219f97a50f64cd6d8313d75df3494ee97b1a3/wrapt-2.0.0.tar.gz", hash = "sha256:35a542cc7a962331d0279735c30995b024e852cf40481e384fd63caaa391cbb9", size = 81722, upload-time = "2025-10-19T23:47:54.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/38/0dd39f83163fd28326afba84e3e416656938df07e60a924ac4d992b30220/wrapt-2.0.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:79a53d86c2aff7b32cc77267e3a308365d1fcb881e74bc9cbe26f63ee90e37f0", size = 78242, upload-time = "2025-10-19T23:46:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/fa7a5c1d73f8690c712f9d2e4615700c6809942536dd3f441b9ba650a310/wrapt-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d731a4f22ed6ffa4cb551b4d2b0c24ff940c27a88edaf8e3490a5ee3a05aef71", size = 61207, upload-time = "2025-10-19T23:46:52.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/67cb93da492eb0a1cb17b7ed18220d059e58f00467ce6728b674d3441b3d/wrapt-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e02ab8c0ac766a5a6e81cd3b6cc39200c69051826243182175555872522bd5a", size = 61748, upload-time = "2025-10-19T23:46:54.468Z" }, + { url = "https://files.pythonhosted.org/packages/e5/be/912bbd70cc614f491b526a1d7fe85695b283deed19287b9f32460178c54d/wrapt-2.0.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:895870602d65d7338edb3b6a717d856632ad9f14f7ff566214e4fb11f0816649", size = 120424, upload-time = "2025-10-19T23:46:57.575Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/10df8937e7da2aa9bc3662a4b623e51a323c68f42cad7b13f0e61a700ce2/wrapt-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b9ad4fab76a0086dc364c4f17f39ad289600e73ef5c6e9ab529aff22cac1ac3", size = 122804, upload-time = "2025-10-19T23:46:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f3/60/576751b1919adab9f63168e3b5fd46c0d1565871b1cc4c2569503ccf4be6/wrapt-2.0.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7ca0562606d7bad2736b2c18f61295d61f50cd3f4bfc51753df13614dbcce1b", size = 117398, upload-time = "2025-10-19T23:46:55.814Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/243411f360cc27bae5f8e21c16f1a8d87674c5534f4558e8a97c1e0d1c6f/wrapt-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe089d9f5a4a3dea0108a8ae34bced114d0c4cca417bada1c5e8f42d98af9050", size = 121230, upload-time = "2025-10-19T23:47:01.347Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/2f21f692c3b3f0857cb82708ce0c341fbac55a489d4025ae4e3fd5d5de8c/wrapt-2.0.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e761f2d2f8dbc80384af3d547b522a80e67db3e319c7b02e7fd97aded0a8a678", size = 116296, upload-time = "2025-10-19T23:47:02.659Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ed/678957fad212cfb1b65b2359d62f5619f5087d1d1cf296c6a996be45171c/wrapt-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:17ba1bdc52d0c783481850996aa26cea5237720769197335abea2ae6b4c23bc0", size = 119602, upload-time = "2025-10-19T23:47:03.775Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e3/aeb4c3b052d3eed95e61babc20dcb1a512651e098cca4b84a6896585c06a/wrapt-2.0.0-cp314-cp314-win32.whl", hash = "sha256:f73318741b141223a4674ba96992aa2291b1b3f7a5e85cb3c2c964f86171eb45", size = 58649, upload-time = "2025-10-19T23:47:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2a/a71c51cb211798405b59172c7df5789a5b934b18317223cf22e0c6f852de/wrapt-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8e08d4edb13cafe7b3260f31d4de033f73d3205774540cf583bffaa4bec97db9", size = 60897, upload-time = "2025-10-19T23:47:04.862Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a5/acc5628035d06f69e9144cca543ca54c33b42a5a23b6f1e8fa131026db89/wrapt-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:af01695c2b7bbd8d67b869d8e3de2b123a7bfbee0185bdd138c2775f75373b83", size = 59306, upload-time = "2025-10-19T23:47:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e6/1318ca07d7fcee57e4592a78dacd9d5493b8ddd971c553a62904fb2c0cf2/wrapt-2.0.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:057f02c13cce7b26c79624c06a3e1c2353e6dc9708525232232f6768118042ca", size = 81987, upload-time = "2025-10-19T23:47:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bf/ffac358ddf61c3923d94a8b0e7620f2af1cd1b637a0fe4963a3919aa62b7/wrapt-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:79bdd84570267f3f43d609c892ae2d30b91ee4b8614c2cbfd311a2965f1c9bdb", size = 62902, upload-time = "2025-10-19T23:47:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/b5/af/387c51f9e7b544fe95d852fc94f9f3866e3f7d7d39c2ee65041752f90bc2/wrapt-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93c8b4f4d54fd401a817abbfc9bf482aa72fd447f8adf19ce81d035b3f5c762c", size = 63635, upload-time = "2025-10-19T23:47:11.746Z" }, + { url = "https://files.pythonhosted.org/packages/7c/99/d38d8c80b9cc352531d4d539a17e3674169a5cc25a7e6e5e3c27bc29893e/wrapt-2.0.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e09ffd31001dce71c2c2a4fc201bdba9a2f9f62b23700cf24af42266e784741", size = 152659, upload-time = "2025-10-19T23:47:15.344Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2a/e154432f274e22ecf2465583386c5ceffa5e0bab3947c1c5b26cc8e7b275/wrapt-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d87c285ff04e26083c4b03546e7b74df7ba4f1f32f1dcb92e9ac13c2dbb4c379", size = 158818, upload-time = "2025-10-19T23:47:17.569Z" }, + { url = "https://files.pythonhosted.org/packages/c5/7a/3a40c453300e2898e99c27495b8109ff7cd526997d12cfb8ebd1843199a4/wrapt-2.0.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e52e50ea0a72ea48d1291cf8b8aaedcc99072d9dc5baba6b820486dcf4c67da8", size = 146113, upload-time = "2025-10-19T23:47:13.026Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e2/3116a9eade8bea2bf5eedba3fa420e3c7d193d4b047440330d8eaf1098de/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fd4c95536975895f32571073446e614d5e2810b666b64955586dcddfd438fd3", size = 155689, upload-time = "2025-10-19T23:47:19.397Z" }, + { url = "https://files.pythonhosted.org/packages/43/1c/277d3fbe9d177830ab9e54fe9253f38455b75a22d639a4bd9fa092d55ae5/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6ebfe9283209220ed9de80a3e9442aab8fc2be5a9bbf8491b99e02ca9349a89", size = 144403, upload-time = "2025-10-19T23:47:20.779Z" }, + { url = "https://files.pythonhosted.org/packages/d8/37/ab6ddaf182248aac5ed925725ef4c69a510594764665ecbd95bdd4481f16/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5d3ebd784804f146b7ea55359beb138e23cc18e5a5cc2cf26ad438723c00ce3a", size = 150307, upload-time = "2025-10-19T23:47:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d7/df9e2d8040a3af618ff9496261cf90ca4f886fd226af0f4a69ac0c020c3b/wrapt-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:9b15940ae9debc8b40b15dc57e1ce4433f7fb9d3f8761c7fab1ddd94cb999d99", size = 60557, upload-time = "2025-10-19T23:47:26.73Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/502bd4557a3a9199ea73cc5932cf83354bd362682162f0b14164d2e90216/wrapt-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a0efbbc06d3e2077476a04f55859819d23206600b4c33f791359a8e6fa3c362", size = 63988, upload-time = "2025-10-19T23:47:23.826Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/632b13942f45db7af709f346ff38b8992c8c21b004e61ab320b0dec525fe/wrapt-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7fec8a9455c029c8cf4ff143a53b6e7c463268d42be6c17efa847ebd2f809965", size = 60584, upload-time = "2025-10-19T23:47:25.396Z" }, + { url = "https://files.pythonhosted.org/packages/00/5c/c34575f96a0a038579683c7f10fca943c15c7946037d1d254ab9db1536ec/wrapt-2.0.0-py3-none-any.whl", hash = "sha256:02482fb0df89857e35427dfb844319417e14fae05878f295ee43fa3bf3b15502", size = 43998, upload-time = "2025-10-19T23:47:52.858Z" }, +] diff --git a/2025/production/models.py b/2025/production/models.py deleted file mode 100644 index 5f2698c8..00000000 --- a/2025/production/models.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy.orm import declarative_base -from sqlalchemy import Column, Integer, String, DateTime -import datetime - -Base = declarative_base() - -class Conversion(Base): - __tablename__ = "conversions" - - id = Column(Integer, primary_key=True) - from_currency = Column(String) - to_currency = Column(String) - amount = Column(Integer) - result = Column(Integer) - timestamp = Column(DateTime, default=datetime.datetime.utcnow) \ No newline at end of file From c79ad862f37c4ed687d52da19bdc48cde7e8720c Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 5 Nov 2025 17:00:04 +0100 Subject: [PATCH 060/113] Worked on code example --- 2025/production/0_starting_point.py | 24 +++++++-- 2025/production/1_validation.py | 32 ++++++++++-- 2025/production/2_testing.py | 2 +- 2025/production/exchange_app/api.py | 9 ++-- 2025/production/exchange_app/config.py | 11 ++-- 2025/production/exchange_app/database.py | 14 +++++- 2025/production/exchange_app/db.sqlite3 | Bin 0 -> 12288 bytes 2025/production/exchange_app/main.py | 17 +++---- 2025/production/exchange_app/models.py | 33 ++++++------ 2025/production/exchange_app/pyproject.toml | 4 ++ 2025/production/exchange_app/seed_rates.py | 8 +-- 2025/production/exchange_app/services.py | 15 +++--- .../production/exchange_app/tests/conftest.py | 28 +++++++---- .../production/exchange_app/tests/test_api.py | 11 ++-- .../exchange_app/tests/test_service.py | 7 +-- 2025/production/exchange_app/uv.lock | 27 ++++++++++ 2025/production/pyproject.toml | 1 + 2025/production/uv.lock | 47 ++++++++++++++++++ 18 files changed, 217 insertions(+), 73 deletions(-) create mode 100644 2025/production/exchange_app/db.sqlite3 diff --git a/2025/production/0_starting_point.py b/2025/production/0_starting_point.py index 361059c7..4dac403d 100644 --- a/2025/production/0_starting_point.py +++ b/2025/production/0_starting_point.py @@ -1,8 +1,26 @@ -from fastapi import FastAPI +import uvicorn +from fastapi import FastAPI, HTTPException app = FastAPI() +# Hardcoded exchange rates (not production-ready!) +RATES = { + ("USD", "EUR"): 0.91, + ("EUR", "USD"): 1.10, + ("USD", "JPY"): 150.0, +} + + @app.get("/convert") def convert(from_currency: str, to_currency: str, amount: float): - rate = 1.1 # hardcoded - return {"result": amount * rate} \ No newline at end of file + key = (from_currency.upper(), to_currency.upper()) + rate = RATES.get(key) + + if rate is None: + raise HTTPException(status_code=400, detail="Exchange rate not available") + + return {"result": amount * rate} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/production/1_validation.py b/2025/production/1_validation.py index d6df66c7..e75e3cdb 100644 --- a/2025/production/1_validation.py +++ b/2025/production/1_validation.py @@ -1,11 +1,33 @@ -from pydantic import condecimal -from fastapi import Query +from decimal import Decimal + +import uvicorn +from fastapi import FastAPI, HTTPException, Query + +app = FastAPI() + +# Hardcoded exchange rates as Decimals +RATES = { + ("USD", "EUR"): Decimal("0.91"), + ("EUR", "USD"): Decimal("1.10"), + ("USD", "JPY"): Decimal("150.0"), +} + @app.get("/convert") def convert( from_currency: str = Query(..., min_length=3, max_length=3), to_currency: str = Query(..., min_length=3, max_length=3), - amount: condecimal(gt=0) = Query(...) + amount: Decimal = Query(..., gt=0), ): - rate = 1.1 - return {"result": float(amount * rate)} \ No newline at end of file + key = (from_currency.upper(), to_currency.upper()) + rate = RATES.get(key) + + if rate is None: + raise HTTPException(status_code=400, detail="Exchange rate not available") + + result = amount * rate # both Decimal + return {"rate": float(rate), "result": float(result)} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/production/2_testing.py b/2025/production/2_testing.py index 68aa3c6a..a6b1a30f 100644 --- a/2025/production/2_testing.py +++ b/2025/production/2_testing.py @@ -1,5 +1,5 @@ from fastapi.testclient import TestClient -from main import app +from 1_validation import app client = TestClient(app) diff --git a/2025/production/exchange_app/api.py b/2025/production/exchange_app/api.py index c75bb188..13b00980 100644 --- a/2025/production/exchange_app/api.py +++ b/2025/production/exchange_app/api.py @@ -1,14 +1,15 @@ +from database import get_db from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import condecimal -from sqlalchemy.orm import Session -from .database import get_db -from .services import ExchangeRateService +from services import ExchangeRateService from slowapi import Limiter from slowapi.util import get_remote_address +from sqlalchemy.orm import Session router = APIRouter() limiter = Limiter(key_func=get_remote_address) + @router.get("/convert") @limiter.limit("5/minute") def convert( @@ -27,4 +28,4 @@ def convert( @router.get("/health") def health(): - return {"status": "ok"} \ No newline at end of file + return {"status": "ok"} diff --git a/2025/production/exchange_app/config.py b/2025/production/exchange_app/config.py index 079c3753..5d0667d3 100644 --- a/2025/production/exchange_app/config.py +++ b/2025/production/exchange_app/config.py @@ -1,14 +1,17 @@ -from pydantic_settings import BaseSettings from functools import lru_cache +from pydantic import ConfigDict +from pydantic_settings import BaseSettings + + class Settings(BaseSettings): sentry_dsn: str = "" log_level: str = "INFO" database_url: str = "sqlite:///./db.sqlite3" - class Config: - env_file = ".env" + model_config = ConfigDict(env_file=".env") + @lru_cache def get_settings(): - return Settings() \ No newline at end of file + return Settings() diff --git a/2025/production/exchange_app/database.py b/2025/production/exchange_app/database.py index 71bc6528..060bb946 100644 --- a/2025/production/exchange_app/database.py +++ b/2025/production/exchange_app/database.py @@ -1,5 +1,15 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -engine = create_engine("sqlite:///./db.sqlite3", connect_args={"check_same_thread": False}) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) \ No newline at end of file +engine = create_engine( + "sqlite:///./db.sqlite3", connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/2025/production/exchange_app/db.sqlite3 b/2025/production/exchange_app/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..9d5d938032c80afc375b3af18fe71f3563ebce2e GIT binary patch literal 12288 zcmeI#Piw+37zXgP4#dL#90p!r@M2-^;ul!wvQ1WXc5&cII*m}U%}C?GqaOFEmyXGb zAndSHc>-zk_RS&Bucy3>!^B8hDxJoLy6l2E4!a>@jJbB!?A(u{KG_%c?UdHXmM**g zygapC_iVz>efz+G00bZa0SG_<0uX=z1Rwx`>IiJdZoS#*IJ=37-&S(2vJa{AL}fD_ z8<`*0wO+&n!AS(SA*aJGwY^5Np#D(s2OiOIBxpDZ!z=O{ORdt`e66+2<{NqqqTXW= zwYz@NGwN8UI0M~>qdum5Fcw literal 0 HcmV?d00001 diff --git a/2025/production/exchange_app/main.py b/2025/production/exchange_app/main.py index 2419f9a2..35252178 100644 --- a/2025/production/exchange_app/main.py +++ b/2025/production/exchange_app/main.py @@ -1,16 +1,15 @@ -from fastapi import FastAPI -from .api import router, limiter -from .models import Base -from .database import engine -from .config import settings import logging -import sentry_sdk + +from api import limiter, router +from config import get_settings +from database import engine +from fastapi import FastAPI +from models import Base Base.metadata.create_all(bind=engine) -sentry_sdk.init(dsn=settings.sentry_dsn) -logging.basicConfig(level=settings.log_level) +logging.basicConfig(level=get_settings().log_level) app = FastAPI() app.include_router(router) -app.state.limiter = limiter \ No newline at end of file +app.state.limiter = limiter diff --git a/2025/production/exchange_app/models.py b/2025/production/exchange_app/models.py index 0db4b703..aba8b869 100644 --- a/2025/production/exchange_app/models.py +++ b/2025/production/exchange_app/models.py @@ -1,24 +1,29 @@ -from sqlalchemy.orm import declarative_base -from sqlalchemy import Column, Integer, String, DateTime, Float import datetime -Base = declarative_base() +from sqlalchemy import DateTime, Float, Integer, String +from sqlalchemy.orm import DeclarativeBase, mapped_column + + +class Base(DeclarativeBase): + pass + class Conversion(Base): __tablename__ = "conversions" - id = Column(Integer, primary_key=True) - from_currency = Column(String(3)) - to_currency = Column(String(3)) - amount = Column(Float) - result = Column(Float) - timestamp = Column(DateTime, default=datetime.datetime.utcnow) + id = mapped_column(Integer, primary_key=True) + from_currency = mapped_column(String(3)) + to_currency = mapped_column(String(3)) + amount = mapped_column(Float) + result = mapped_column(Float) + timestamp = mapped_column(DateTime, default=datetime.datetime.now) + class ConversionRate(Base): __tablename__ = "conversion_rates" - id = Column(Integer, primary_key=True) - from_currency = Column(String(3)) - to_currency = Column(String(3)) - rate = Column(Float) - timestamp = Column(DateTime, default=datetime.datetime.utcnow) \ No newline at end of file + id = mapped_column(Integer, primary_key=True) + from_currency = mapped_column(String(3)) + to_currency = mapped_column(String(3)) + rate = mapped_column(Float) + timestamp = mapped_column(DateTime, default=datetime.datetime.now) diff --git a/2025/production/exchange_app/pyproject.toml b/2025/production/exchange_app/pyproject.toml index 74ece992..3b77c492 100644 --- a/2025/production/exchange_app/pyproject.toml +++ b/2025/production/exchange_app/pyproject.toml @@ -6,8 +6,12 @@ dependencies = [ "alembic>=1.17.1", "fastapi>=0.120.2", "httpx>=0.28.1", + "pydantic-settings>=2.11.0", "pytest>=8.4.2", "slowapi>=0.1.9", "sqlalchemy>=2.0.44", "uvicorn>=0.38.0", ] + +[tool.pytest.ini_options] +pythonpath = "." diff --git a/2025/production/exchange_app/seed_rates.py b/2025/production/exchange_app/seed_rates.py index a7856ff8..f0acf559 100644 --- a/2025/production/exchange_app/seed_rates.py +++ b/2025/production/exchange_app/seed_rates.py @@ -1,6 +1,6 @@ +from database import SessionLocal +from models import ConversionRate from sqlalchemy.orm import Session -from .models import ConversionRate -from .database import SessionLocal sample_rates = [ {"from_currency": "USD", "to_currency": "EUR", "rate": 0.91}, @@ -10,6 +10,7 @@ {"from_currency": "USD", "to_currency": "GBP", "rate": 0.78}, ] + def main(): db: Session = SessionLocal() for entry in sample_rates: @@ -19,5 +20,6 @@ def main(): db.close() print("Seeded exchange rates.") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/2025/production/exchange_app/services.py b/2025/production/exchange_app/services.py index 88d115c1..b7df2da3 100644 --- a/2025/production/exchange_app/services.py +++ b/2025/production/exchange_app/services.py @@ -1,8 +1,10 @@ -from sqlalchemy.orm import Session -from .models import ConversionRate, Conversion -from fastapi import HTTPException import datetime +from fastapi import HTTPException +from models import Conversion, ConversionRate +from sqlalchemy.orm import Session + + class ExchangeRateService: def __init__(self, db: Session): self.db = db @@ -28,12 +30,9 @@ def convert(self, from_currency: str, to_currency: str, amount: float) -> dict: to_currency=to_currency, amount=amount, result=result, - timestamp=datetime.datetime.utcnow() + timestamp=datetime.datetime.now(), ) self.db.add(conversion) self.db.commit() - return { - "rate": rate_entry.rate, - "result": result - } \ No newline at end of file + return {"rate": rate_entry.rate, "result": result} diff --git a/2025/production/exchange_app/tests/conftest.py b/2025/production/exchange_app/tests/conftest.py index 7c22a018..10e1f33d 100644 --- a/2025/production/exchange_app/tests/conftest.py +++ b/2025/production/exchange_app/tests/conftest.py @@ -1,20 +1,24 @@ import pytest +from database import get_db from fastapi.testclient import TestClient -from sqlalchemy import create_engine +from main import app +from models import Base +from sqlalchemy import StaticPool, create_engine from sqlalchemy.orm import sessionmaker -from exchange_app.models import Base -from exchange_app.main import app -from exchange_app.database import get_db - # Use in-memory SQLite for tests -TEST_DATABASE_URL = "sqlite:///:memory:" - -engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False}) +DATABASE_URL = "sqlite:///:memory:" +engine = create_engine( + DATABASE_URL, + connect_args={ + "check_same_thread": False, + }, + poolclass=StaticPool, +) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + # Override the get_db dependency -@pytest.fixture(autouse=True) def override_get_db(): Base.metadata.create_all(bind=engine) db = TestingSessionLocal() @@ -23,16 +27,20 @@ def override_get_db(): finally: db.close() + app.dependency_overrides[get_db] = override_get_db + @pytest.fixture def client(): return TestClient(app) + @pytest.fixture def db_session(): + Base.metadata.create_all(bind=engine) db = TestingSessionLocal() try: yield db finally: - db.close() \ No newline at end of file + db.close() diff --git a/2025/production/exchange_app/tests/test_api.py b/2025/production/exchange_app/tests/test_api.py index 2b49e5a3..8876bbb2 100644 --- a/2025/production/exchange_app/tests/test_api.py +++ b/2025/production/exchange_app/tests/test_api.py @@ -1,12 +1,9 @@ -from ..models import ConversionRate +from models import ConversionRate + def test_convert_success(client, db_session): # Arrange: Seed rate - db_session.add(ConversionRate( - from_currency="USD", - to_currency="EUR", - rate=0.9 - )) + db_session.add(ConversionRate(from_currency="USD", to_currency="EUR", rate=0.9)) db_session.commit() # Act @@ -25,4 +22,4 @@ def test_convert_missing_rate(client): # Assert assert response.status_code == 500 # Internal server error from missing rate - assert "Exchange rate not found" in response.json()["detail"] \ No newline at end of file + assert "Exchange rate not found" in response.json()["detail"] diff --git a/2025/production/exchange_app/tests/test_service.py b/2025/production/exchange_app/tests/test_service.py index ed2593c5..14359fc5 100644 --- a/2025/production/exchange_app/tests/test_service.py +++ b/2025/production/exchange_app/tests/test_service.py @@ -1,5 +1,6 @@ -from exchange_app.models import ConversionRate -from exchange_app.services import ExchangeRateService +from models import ConversionRate +from services import ExchangeRateService + def test_service_convert_valid(db_session): db_session.add(ConversionRate(from_currency="USD", to_currency="JPY", rate=150)) @@ -18,4 +19,4 @@ def test_service_convert_invalid_currency(db_session): service.convert("AAA", "BBB", 10) assert False, "Expected HTTPException" except Exception as e: - assert "Exchange rate not found" in str(e) \ No newline at end of file + assert "Exchange rate not found" in str(e) diff --git a/2025/production/exchange_app/uv.lock b/2025/production/exchange_app/uv.lock index 01b782e7..1b4fcb57 100644 --- a/2025/production/exchange_app/uv.lock +++ b/2025/production/exchange_app/uv.lock @@ -97,6 +97,7 @@ dependencies = [ { name = "alembic" }, { name = "fastapi" }, { name = "httpx" }, + { name = "pydantic-settings" }, { name = "pytest" }, { name = "slowapi" }, { name = "sqlalchemy" }, @@ -108,6 +109,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.17.1" }, { name = "fastapi", specifier = ">=0.120.2" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "slowapi", specifier = ">=0.1.9" }, { name = "sqlalchemy", specifier = ">=2.0.44" }, @@ -141,6 +143,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] @@ -318,6 +322,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -343,6 +361,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + [[package]] name = "slowapi" version = "0.1.9" diff --git a/2025/production/pyproject.toml b/2025/production/pyproject.toml index 5e382056..4b0c20f9 100644 --- a/2025/production/pyproject.toml +++ b/2025/production/pyproject.toml @@ -6,6 +6,7 @@ dependencies = [ "alembic>=1.17.1", "fastapi>=0.120.2", "httpx>=0.28.1", + "pytest>=8.4.2", "slowapi>=0.1.9", "sqlalchemy>=2.0.44", "uvicorn>=0.38.0", diff --git a/2025/production/uv.lock b/2025/production/uv.lock index e0f454e9..0f69ff18 100644 --- a/2025/production/uv.lock +++ b/2025/production/uv.lock @@ -116,6 +116,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] @@ -165,6 +167,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "limits" version = "5.6.0" @@ -230,6 +241,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "production" version = "0.1.0" @@ -238,6 +258,7 @@ dependencies = [ { name = "alembic" }, { name = "fastapi" }, { name = "httpx" }, + { name = "pytest" }, { name = "slowapi" }, { name = "sqlalchemy" }, { name = "uvicorn" }, @@ -248,6 +269,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.17.1" }, { name = "fastapi", specifier = ">=0.120.2" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=8.4.2" }, { name = "slowapi", specifier = ">=0.1.9" }, { name = "sqlalchemy", specifier = ">=2.0.44" }, { name = "uvicorn", specifier = ">=0.38.0" }, @@ -298,6 +320,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "slowapi" version = "0.1.9" From 86e54026773cf4ff12c469edb07af68927d336f6 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 5 Nov 2025 17:06:48 +0100 Subject: [PATCH 061/113] Minor improvements --- 2025/production/exchange_app/models.py | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/2025/production/exchange_app/models.py b/2025/production/exchange_app/models.py index aba8b869..8714c79b 100644 --- a/2025/production/exchange_app/models.py +++ b/2025/production/exchange_app/models.py @@ -1,7 +1,7 @@ -import datetime +from datetime import datetime -from sqlalchemy import DateTime, Float, Integer, String -from sqlalchemy.orm import DeclarativeBase, mapped_column +from sqlalchemy import Integer, String +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): @@ -11,19 +11,19 @@ class Base(DeclarativeBase): class Conversion(Base): __tablename__ = "conversions" - id = mapped_column(Integer, primary_key=True) - from_currency = mapped_column(String(3)) - to_currency = mapped_column(String(3)) - amount = mapped_column(Float) - result = mapped_column(Float) - timestamp = mapped_column(DateTime, default=datetime.datetime.now) + id: Mapped[int] = mapped_column(primary_key=True) + from_currency: Mapped[str] = mapped_column(String(3)) + to_currency: Mapped[str] = mapped_column(String(3)) + amount: Mapped[float] = mapped_column() + result: Mapped[float] = mapped_column() + timestamp: Mapped[datetime] = mapped_column(default=datetime.now) class ConversionRate(Base): __tablename__ = "conversion_rates" - id = mapped_column(Integer, primary_key=True) - from_currency = mapped_column(String(3)) - to_currency = mapped_column(String(3)) - rate = mapped_column(Float) - timestamp = mapped_column(DateTime, default=datetime.datetime.now) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + from_currency: Mapped[str] = mapped_column(String(3)) + to_currency: Mapped[str] = mapped_column(String(3)) + rate: Mapped[float] = mapped_column() + timestamp: Mapped[datetime] = mapped_column(default=datetime.now) From f4fbe3042266c9405b8f17e58243382d334fa01c Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 5 Nov 2025 17:11:15 +0100 Subject: [PATCH 062/113] Fixed slowapi warning --- 2025/production/exchange_app/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/2025/production/exchange_app/pyproject.toml b/2025/production/exchange_app/pyproject.toml index 3b77c492..9a87f32a 100644 --- a/2025/production/exchange_app/pyproject.toml +++ b/2025/production/exchange_app/pyproject.toml @@ -15,3 +15,6 @@ dependencies = [ [tool.pytest.ini_options] pythonpath = "." +filterwarnings = [ + "ignore:'asyncio.iscoroutinefunction' is deprecated:DeprecationWarning:slowapi.extension" +] From 2d753518d1064dde0b7e70acbb2b0405c89766fe Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 6 Nov 2025 15:00:36 +0100 Subject: [PATCH 063/113] More work on code example. --- 2025/production/1_types.py | 28 ++++++++++++++++++ 2025/production/2_testing.py | 9 ------ .../{1_validation.py => 2_validation.py} | 0 2025/production/exchange_app/.env.example | 1 - 2025/production/exchange_app/api.py | 5 ++-- 2025/production/exchange_app/config.py | 2 +- 2025/production/exchange_app/database.py | 7 ++++- 2025/production/exchange_app/db.sqlite3 | Bin 12288 -> 12288 bytes 2025/production/exchange_app/main.py | 4 +++ 2025/production/exchange_app/services.py | 1 + .../production/exchange_app/tests/conftest.py | 6 ++-- 11 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 2025/production/1_types.py delete mode 100644 2025/production/2_testing.py rename 2025/production/{1_validation.py => 2_validation.py} (100%) diff --git a/2025/production/1_types.py b/2025/production/1_types.py new file mode 100644 index 00000000..97b79657 --- /dev/null +++ b/2025/production/1_types.py @@ -0,0 +1,28 @@ +from decimal import Decimal + +import uvicorn +from fastapi import FastAPI, HTTPException + +app = FastAPI() + +# Hardcoded exchange rates (not production-ready!) +RATES = { + ("USD", "EUR"): Decimal("0.91"), + ("EUR", "USD"): Decimal("1.10"), + ("USD", "JPY"): Decimal("150.0"), +} + + +@app.get("/convert") +def convert(from_currency: str, to_currency: str, amount: Decimal): + key = (from_currency.upper(), to_currency.upper()) + rate = RATES.get(key) + + if rate is None: + raise HTTPException(status_code=400, detail="Exchange rate not available") + + return {"result": amount * rate} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/production/2_testing.py b/2025/production/2_testing.py deleted file mode 100644 index a6b1a30f..00000000 --- a/2025/production/2_testing.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi.testclient import TestClient -from 1_validation import app - -client = TestClient(app) - -def test_convert(): - res = client.get("/convert?from_currency=USD&to_currency=EUR&amount=100") - assert res.status_code == 200 - assert "result" in res.json() \ No newline at end of file diff --git a/2025/production/1_validation.py b/2025/production/2_validation.py similarity index 100% rename from 2025/production/1_validation.py rename to 2025/production/2_validation.py diff --git a/2025/production/exchange_app/.env.example b/2025/production/exchange_app/.env.example index 176e9275..7d9f83a4 100644 --- a/2025/production/exchange_app/.env.example +++ b/2025/production/exchange_app/.env.example @@ -1,4 +1,3 @@ api_url=https://api.exchangerate.host/latest log_level=INFO -sentry_dsn= database_url=sqlite:///./db.sqlite3 \ No newline at end of file diff --git a/2025/production/exchange_app/api.py b/2025/production/exchange_app/api.py index 13b00980..dbf2c29b 100644 --- a/2025/production/exchange_app/api.py +++ b/2025/production/exchange_app/api.py @@ -1,6 +1,7 @@ +from decimal import Decimal + from database import get_db from fastapi import APIRouter, Depends, HTTPException, Query, Request -from pydantic import condecimal from services import ExchangeRateService from slowapi import Limiter from slowapi.util import get_remote_address @@ -16,7 +17,7 @@ def convert( request: Request, from_currency: str = Query(..., min_length=3, max_length=3), to_currency: str = Query(..., min_length=3, max_length=3), - amount: condecimal(gt=0) = Query(...), + amount: Decimal = Query(..., gt=0), db: Session = Depends(get_db), ): try: diff --git a/2025/production/exchange_app/config.py b/2025/production/exchange_app/config.py index 5d0667d3..f8a4f70a 100644 --- a/2025/production/exchange_app/config.py +++ b/2025/production/exchange_app/config.py @@ -5,7 +5,7 @@ class Settings(BaseSettings): - sentry_dsn: str = "" + api_url: str = "" log_level: str = "INFO" database_url: str = "sqlite:///./db.sqlite3" diff --git a/2025/production/exchange_app/database.py b/2025/production/exchange_app/database.py index 060bb946..7b09fcd0 100644 --- a/2025/production/exchange_app/database.py +++ b/2025/production/exchange_app/database.py @@ -1,11 +1,16 @@ +from config import get_settings +from models import Base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine = create_engine( - "sqlite:///./db.sqlite3", connect_args={"check_same_thread": False} + get_settings().database_url, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# Ensure tables are created +Base.metadata.create_all(bind=engine) + def get_db(): db = SessionLocal() diff --git a/2025/production/exchange_app/db.sqlite3 b/2025/production/exchange_app/db.sqlite3 index 9d5d938032c80afc375b3af18fe71f3563ebce2e..89d2fb3dbbaac8e57958ac8010e40f069d2e8540 100644 GIT binary patch literal 12288 zcmeI$O=}ZD7zgm#P3qEsU7@8tse`>FV3ygJyzE}mZM%)pG)=dO5D-dCw~*3gWp`6h zPg=Zq@g^u9`~;po`vJtS;7t$@f_n1cY)eZT8e1=V@c+Q>KFd5a%=~W8Y^+zikw>@v zpx2J5PA(BaAPbZdLL~kq_;VB@Pm)JJ{3{IQ<5GzzPiHRjQE@6srhf7l0s;_#00bZa z0SG_<0uX=z1pb-8qk$;RWwXNm<*0pk*X#KGJue8met$D)M_xGkpICAoyW!A=y;ybV z=qOF6Cc9g-QfoM6$ECHk2CX%#)$24hxgGeu&CVbQyng3Cy=}WoH*7bpXHFDF{+T85 zuINg2&E`uFEgyAzUKq7|_vkX;UZb+=oEWaVl~vnq(OXW7rn_62ROYcDNOSY^!hU7s z+(SnW#WAN1|H47rJ%7-TPC07eg@fJG2L9L}?#+{Ok;w1mSMm-|5D1Rwwb2tWV= z5P$##AOHaf91F0dM5d<&$!=~eJ54vWa7{l5jcK{8s%BZ9syT}>i+yft{HhtV@km=S&_y4|AahC!nZM-#xpN{xRI!^G-c)sfMEE3I$VF zl85!YnBNDKzCHL@elzoMe0@3**NgGx!OeP$JRe)BDdOSE{Ei`B`Rj`~UjC+DINmv* RQu=b`?fT~@SI5;C^ dict: .order_by(ConversionRate.timestamp.desc()) .first() ) + print(rate_entry) if not rate_entry or rate_entry.rate <= 0: raise HTTPException(status_code=404, detail="Exchange rate not found") diff --git a/2025/production/exchange_app/tests/conftest.py b/2025/production/exchange_app/tests/conftest.py index 10e1f33d..0ca5f30f 100644 --- a/2025/production/exchange_app/tests/conftest.py +++ b/2025/production/exchange_app/tests/conftest.py @@ -28,9 +28,6 @@ def override_get_db(): db.close() -app.dependency_overrides[get_db] = override_get_db - - @pytest.fixture def client(): return TestClient(app) @@ -44,3 +41,6 @@ def db_session(): yield db finally: db.close() + + +app.dependency_overrides[get_db] = override_get_db From 107d3146dbe717598978fac93ec1cf47f91f7277 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 6 Nov 2025 15:05:42 +0100 Subject: [PATCH 064/113] More code example cleanup. --- 2025/production/1_types.py | 28 --------------- 2025/production/2_validation.py | 33 ------------------ 2025/production/3_logging.py | 22 ------------ 2025/production/4_service.py | 15 -------- 2025/production/5_config.py | 12 ------- 2025/production/6_security.py | 14 -------- 2025/production/exchange_app/db.sqlite3 | Bin 12288 -> 0 bytes 2025/production/exchange_app/services.py | 3 +- ...{0_starting_point.py => starting_point.py} | 3 +- 9 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 2025/production/1_types.py delete mode 100644 2025/production/2_validation.py delete mode 100644 2025/production/3_logging.py delete mode 100644 2025/production/4_service.py delete mode 100644 2025/production/5_config.py delete mode 100644 2025/production/6_security.py delete mode 100644 2025/production/exchange_app/db.sqlite3 rename 2025/production/{0_starting_point.py => starting_point.py} (91%) diff --git a/2025/production/1_types.py b/2025/production/1_types.py deleted file mode 100644 index 97b79657..00000000 --- a/2025/production/1_types.py +++ /dev/null @@ -1,28 +0,0 @@ -from decimal import Decimal - -import uvicorn -from fastapi import FastAPI, HTTPException - -app = FastAPI() - -# Hardcoded exchange rates (not production-ready!) -RATES = { - ("USD", "EUR"): Decimal("0.91"), - ("EUR", "USD"): Decimal("1.10"), - ("USD", "JPY"): Decimal("150.0"), -} - - -@app.get("/convert") -def convert(from_currency: str, to_currency: str, amount: Decimal): - key = (from_currency.upper(), to_currency.upper()) - rate = RATES.get(key) - - if rate is None: - raise HTTPException(status_code=400, detail="Exchange rate not available") - - return {"result": amount * rate} - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/production/2_validation.py b/2025/production/2_validation.py deleted file mode 100644 index e75e3cdb..00000000 --- a/2025/production/2_validation.py +++ /dev/null @@ -1,33 +0,0 @@ -from decimal import Decimal - -import uvicorn -from fastapi import FastAPI, HTTPException, Query - -app = FastAPI() - -# Hardcoded exchange rates as Decimals -RATES = { - ("USD", "EUR"): Decimal("0.91"), - ("EUR", "USD"): Decimal("1.10"), - ("USD", "JPY"): Decimal("150.0"), -} - - -@app.get("/convert") -def convert( - from_currency: str = Query(..., min_length=3, max_length=3), - to_currency: str = Query(..., min_length=3, max_length=3), - amount: Decimal = Query(..., gt=0), -): - key = (from_currency.upper(), to_currency.upper()) - rate = RATES.get(key) - - if rate is None: - raise HTTPException(status_code=400, detail="Exchange rate not available") - - result = amount * rate # both Decimal - return {"rate": float(rate), "result": float(result)} - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/2025/production/3_logging.py b/2025/production/3_logging.py deleted file mode 100644 index bee5613d..00000000 --- a/2025/production/3_logging.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging -import sentry_sdk -from fastapi import HTTPException, Request - -sentry_sdk.init(dsn=settings.sentry_dsn) -logging.basicConfig(level=settings.log_level) - -@app.get("/convert") -def convert(..., request: Request): - try: - rate = 1.1 # or from a service - result = float(amount * rate) - logging.info(f"Converted {amount} {from_currency} โ†’ {to_currency}") - return {"result": result} - except Exception as e: - logging.error(f"Conversion failed: {e}") - sentry_sdk.capture_exception(e) - raise HTTPException(status_code=500, detail="Internal error") - -@app.get("/health") -def health(): - return {"status": "ok"} \ No newline at end of file diff --git a/2025/production/4_service.py b/2025/production/4_service.py deleted file mode 100644 index 78c4548c..00000000 --- a/2025/production/4_service.py +++ /dev/null @@ -1,15 +0,0 @@ -import httpx - -class ExchangeRateService: - def __init__(self, api_url: str): - self.api_url = api_url - - def get_rate(self, from_currency: str, to_currency: str) -> float: - url = f"{self.api_url}?base={from_currency}&symbols={to_currency}" - response = httpx.get(url, timeout=5.0) - response.raise_for_status() - data = response.json() - rate = data["rates"].get(to_currency) - if not rate or rate <= 0: - raise ValueError("Invalid exchange rate") - return rate \ No newline at end of file diff --git a/2025/production/5_config.py b/2025/production/5_config.py deleted file mode 100644 index ab303812..00000000 --- a/2025/production/5_config.py +++ /dev/null @@ -1,12 +0,0 @@ -# config.py -from pydantic_settings import BaseSettings - -class Settings(BaseSettings): - api_url: str = "https://api.exchangerate.host/latest" - sentry_dsn: str = "" - log_level: str = "INFO" - - class Config: - env_file = ".env" - -settings = Settings() \ No newline at end of file diff --git a/2025/production/6_security.py b/2025/production/6_security.py deleted file mode 100644 index 341e1ad0..00000000 --- a/2025/production/6_security.py +++ /dev/null @@ -1,14 +0,0 @@ -from slowapi import Limiter -from slowapi.util import get_remote_address -from fastapi import Depends - -limiter = Limiter(key_func=get_remote_address) -app.state.limiter = limiter - -def get_service(): - return rate_service - -@app.get("/convert") -@limiter.limit("5/minute") -def convert(..., service: ExchangeRateService = Depends(get_service)): - ... \ No newline at end of file diff --git a/2025/production/exchange_app/db.sqlite3 b/2025/production/exchange_app/db.sqlite3 deleted file mode 100644 index 89d2fb3dbbaac8e57958ac8010e40f069d2e8540..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI$O=}ZD7zgm#P3qEsU7@8tse`>FV3ygJyzE}mZM%)pG)=dO5D-dCw~*3gWp`6h zPg=Zq@g^u9`~;po`vJtS;7t$@f_n1cY)eZT8e1=V@c+Q>KFd5a%=~W8Y^+zikw>@v zpx2J5PA(BaAPbZdLL~kq_;VB@Pm)JJ{3{IQ<5GzzPiHRjQE@6srhf7l0s;_#00bZa z0SG_<0uX=z1pb-8qk$;RWwXNm<*0pk*X#KGJue8met$D)M_xGkpICAoyW!A=y;ybV z=qOF6Cc9g-QfoM6$ECHk2CX%#)$24hxgGeu&CVbQyng3Cy=}WoH*7bpXHFDF{+T85 zuINg2&E`uFEgyAzUKq7|_vkX;UZb+=oEWaVl~vnq(OXW7rn_62ROYcDNOSY^!hU7s z+(SnW#WAN1|H47rJ%7-TPC07eg@fJG2L9L}?#+{Ok;w1mSMm-|5D1Rwwb2tWV= z5P$##AOHaf91F0dM5d<&$!=~eJ54vWa7{l5jcK{8s%BZ9syT}>i+yft{HhtV@km=S&_y4|AahC!nZM-#xpN{xRI!^G-c)sfMEE3I$VF zl85!YnBNDKzCHL@elzoMe0@3**NgGx!OeP$JRe)BDdOSE{Ei`B`Rj`~UjC+DINmv* RQu=b`?fT~@SI5;C^ dict: .order_by(ConversionRate.timestamp.desc()) .first() ) - print(rate_entry) if not rate_entry or rate_entry.rate <= 0: raise HTTPException(status_code=404, detail="Exchange rate not found") + logging.info(f"Using rate: {rate_entry.rate}") result = amount * rate_entry.rate conversion = Conversion( diff --git a/2025/production/0_starting_point.py b/2025/production/starting_point.py similarity index 91% rename from 2025/production/0_starting_point.py rename to 2025/production/starting_point.py index 4dac403d..1cfa7dbc 100644 --- a/2025/production/0_starting_point.py +++ b/2025/production/starting_point.py @@ -3,7 +3,7 @@ app = FastAPI() -# Hardcoded exchange rates (not production-ready!) +# Exchange rates RATES = { ("USD", "EUR"): 0.91, ("EUR", "USD"): 1.10, @@ -19,6 +19,7 @@ def convert(from_currency: str, to_currency: str, amount: float): if rate is None: raise HTTPException(status_code=400, detail="Exchange rate not available") + print(f"Using rate {rate}") return {"result": amount * rate} From c02cf4e8566297638b94127ade123a80c64bbff4 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 10 Nov 2025 16:14:25 +0100 Subject: [PATCH 065/113] Added retry example --- 2025/retry/1_naive.py | 18 ++ 2025/retry/2_retry_function.py | 38 ++++ 2025/retry/3_exponential_backoff.py | 39 ++++ 2025/retry/4_retry_decorator.py | 51 +++++ 2025/retry/5_llm_example.py | 77 ++++++++ 2025/retry/6_fallback_api.py | 66 +++++++ 2025/retry/7_tenacity.py | 25 +++ 2025/retry/pyproject.toml | 10 + 2025/retry/uv.lock | 283 ++++++++++++++++++++++++++++ 9 files changed, 607 insertions(+) create mode 100644 2025/retry/1_naive.py create mode 100644 2025/retry/2_retry_function.py create mode 100644 2025/retry/3_exponential_backoff.py create mode 100644 2025/retry/4_retry_decorator.py create mode 100644 2025/retry/5_llm_example.py create mode 100644 2025/retry/6_fallback_api.py create mode 100644 2025/retry/7_tenacity.py create mode 100644 2025/retry/pyproject.toml create mode 100644 2025/retry/uv.lock diff --git a/2025/retry/1_naive.py b/2025/retry/1_naive.py new file mode 100644 index 00000000..a40b52a6 --- /dev/null +++ b/2025/retry/1_naive.py @@ -0,0 +1,18 @@ +import httpx + + +def fetch_joke() -> str: + """Fetch a random Chuck Norris joke from the API.""" + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random") + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def main() -> None: + print(fetch_joke()) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/2_retry_function.py b/2025/retry/2_retry_function.py new file mode 100644 index 00000000..f4045fab --- /dev/null +++ b/2025/retry/2_retry_function.py @@ -0,0 +1,38 @@ +import random +import time +from typing import Callable + +import httpx + + +def retry[T](operation: Callable[[], T], retries: int = 3, delay: float = 1.0) -> T: + """Retry an operation several times before failing.""" + for attempt in range(1, retries + 1): + try: + return operation() + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == retries: + raise + time.sleep(delay) + + +def fetch_joke() -> str: + """Fetch a random Chuck Norris joke from the API.""" + # randomly raise an error to simulate failures + if random.random() < 0.5: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def main() -> None: + joke: str = retry(fetch_joke, retries=3, delay=1.0) + print(joke) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/3_exponential_backoff.py b/2025/retry/3_exponential_backoff.py new file mode 100644 index 00000000..70b327bb --- /dev/null +++ b/2025/retry/3_exponential_backoff.py @@ -0,0 +1,39 @@ +import random +import time + +import httpx + + +def retry(operation, retries: int = 3, delay: float = 1.0, backoff: float = 2.0): + """Retry an operation using exponential backoff.""" + for attempt in range(1, retries + 1): + try: + return operation() + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == retries: + raise + sleep_time = delay * (backoff ** (attempt - 1)) + print(f"Retrying in {sleep_time:.1f} seconds...") + time.sleep(sleep_time) + + +def fetch_joke() -> str: + """Fetch a random Chuck Norris joke.""" + # randomly raise an error to simulate failures + if random.random() < 0.5: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def main() -> None: + joke: str = retry(fetch_joke, retries=3, delay=1.0, backoff=2.0) + print(joke) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/4_retry_decorator.py b/2025/retry/4_retry_decorator.py new file mode 100644 index 00000000..f593a41b --- /dev/null +++ b/2025/retry/4_retry_decorator.py @@ -0,0 +1,51 @@ +import random +import time +from functools import wraps +from typing import Any, Callable + +import httpx + + +def retry_decorator[T]( + retries: int = 3, delay: float = 1.0, backoff: float = 2.0 +) -> Callable[[Callable[..., T]], Callable[..., T]]: + """A decorator that retries the wrapped function on failure.""" + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + for attempt in range(1, retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == retries: + raise + sleep_time = delay * (backoff ** (attempt - 1)) + print(f"Retrying in {sleep_time:.1f} seconds...") + time.sleep(sleep_time) + raise RuntimeError("All retries failed") + + return wrapper + + return decorator + + +@retry_decorator(retries=4, delay=1.0) +def fetch_joke() -> str: + """Fetch a random Chuck Norris joke.""" + # randomly raise an error to simulate failures + if random.random() < 0.5: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + return response.json()["value"] + + +def main() -> None: + print(fetch_joke()) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/5_llm_example.py b/2025/retry/5_llm_example.py new file mode 100644 index 00000000..41ce881f --- /dev/null +++ b/2025/retry/5_llm_example.py @@ -0,0 +1,77 @@ +import json +import os +import time +from functools import wraps +from typing import Any, Callable + +from dotenv import load_dotenv +from openai import OpenAI + +load_dotenv() + + +def retry_decorator[T]( + retries: int = 3, delay: float = 1.0, backoff: float = 2.0 +) -> Callable[[Callable[..., T]], Callable[..., T]]: + """A decorator that retries the wrapped function on failure.""" + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + for attempt in range(1, retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == retries: + raise + sleep_time = delay * (backoff ** (attempt - 1)) + print(f"Retrying in {sleep_time:.1f} seconds...") + time.sleep(sleep_time) + raise RuntimeError("All retries failed") + + return wrapper + + return decorator + + +@retry_decorator(retries=3, delay=1.0, backoff=2.0) +def get_user_info_with_retry(text: str, api_key: str) -> dict: + client = OpenAI(api_key=api_key) + + response = client.responses.create( + model="gpt-4.1-nano", + instructions=( + "You are a helpful assistant that extracts information and returns it " + "as a valid JSON object. Always return valid JSON with keys 'name' and 'age'." + ), + input=f"Extract the user's name and age from this text: {text}", + ) + + content = response.output_text + return json.loads(content) + + +def main() -> None: + """Example main function to demonstrate retrying invalid JSON responses.""" + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise RuntimeError("Please set your OPENAI_API_KEY environment variable.") + + text = "Hi, my name is Alice and Iโ€™m 30 years old." + + print("Requesting user info from the LLM...\n") + try: + user_info = get_user_info_with_retry(text, api_key) + print("โœ… Successfully parsed JSON:") + print(user_info) + except json.JSONDecodeError as e: + print("โŒ The LLM returned invalid JSON after several retries:") + print(e) + except Exception as e: + print("โŒ An unexpected error occurred:") + print(e) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/6_fallback_api.py b/2025/retry/6_fallback_api.py new file mode 100644 index 00000000..e275805d --- /dev/null +++ b/2025/retry/6_fallback_api.py @@ -0,0 +1,66 @@ +import random +import time +from functools import wraps +from typing import Any, Callable + +import httpx + + +def retry_decorator[T]( + retries: int = 3, delay: float = 1.0, backoff: float = 2.0 +) -> Callable[[Callable[..., T]], Callable[..., T]]: + """A decorator that retries the wrapped function on failure.""" + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + for attempt in range(1, retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == retries: + raise + sleep_time = delay * (backoff ** (attempt - 1)) + print(f"Retrying in {sleep_time:.1f} seconds...") + time.sleep(sleep_time) + raise RuntimeError("All retries failed") + + return wrapper + + return decorator + + +@retry_decorator(retries=3, delay=1.0) +def fetch_main_api() -> str: + """Fetch from the main Chuck Norris API.""" + # randomly raise an error to simulate failures + if random.random() < 0.5: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def fetch_backup_api() -> str: + """Fallback if the main API fails.""" + return "Backup API: Chuck Norris can delete the Recycle Bin." + + +def get_joke() -> str: + """Try the main API, then fallback to backup if retries fail.""" + try: + return fetch_main_api() + except Exception: + print("Main API failed. Switching to backup API.") + return fetch_backup_api() + + +def main() -> None: + print(get_joke()) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/7_tenacity.py b/2025/retry/7_tenacity.py new file mode 100644 index 00000000..5fe628f4 --- /dev/null +++ b/2025/retry/7_tenacity.py @@ -0,0 +1,25 @@ +import random + +import httpx +from tenacity import retry, stop_after_attempt, wait_exponential + + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10)) +def fetch_joke() -> str: + """Fetch a random Chuck Norris joke with retry logic using tenacity.""" + # randomly raise an error to simulate failures + if random.random() < 0.5: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def main() -> None: + print(fetch_joke()) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/pyproject.toml b/2025/retry/pyproject.toml new file mode 100644 index 00000000..cf745090 --- /dev/null +++ b/2025/retry/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "retry" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "httpx>=0.28.1", + "openai>=2.7.1", + "python-dotenv>=1.2.1", + "tenacity>=9.1.2", +] diff --git a/2025/retry/uv.lock b/2025/retry/uv.lock new file mode 100644 index 00000000..bcc8ff8d --- /dev/null +++ b/2025/retry/uv.lock @@ -0,0 +1,283 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, +] + +[[package]] +name = "openai" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/a2/f4023c1e0c868a6a5854955b3374f17153388aed95e835af114a17eac95b/openai-2.7.1.tar.gz", hash = "sha256:df4d4a3622b2df3475ead8eb0fbb3c27fd1c070fa2e55d778ca4f40e0186c726", size = 595933, upload-time = "2025-11-04T06:07:23.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/74/6bfc3adc81f6c2cea4439f2a734c40e3a420703bbcdc539890096a732bbd/openai-2.7.1-py3-none-any.whl", hash = "sha256:2f2530354d94c59c614645a4662b9dab0a5b881c5cd767a8587398feac0c9021", size = 1008780, upload-time = "2025-11-04T06:07:20.818Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "retry" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "openai" }, + { name = "python-dotenv" }, + { name = "tenacity" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "openai", specifier = ">=2.7.1" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "tenacity", specifier = ">=9.1.2" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] From adcd8c7c27d4b13df3ce0b155af669f7d4d58f10 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 18 Nov 2025 14:01:43 +0100 Subject: [PATCH 066/113] Added one more example --- 2025/retry/7_fallback_api_v2.py | 44 +++++++++++++++++++++ 2025/retry/{7_tenacity.py => 8_tenacity.py} | 0 2 files changed, 44 insertions(+) create mode 100644 2025/retry/7_fallback_api_v2.py rename 2025/retry/{7_tenacity.py => 8_tenacity.py} (100%) diff --git a/2025/retry/7_fallback_api_v2.py b/2025/retry/7_fallback_api_v2.py new file mode 100644 index 00000000..35a6b0ec --- /dev/null +++ b/2025/retry/7_fallback_api_v2.py @@ -0,0 +1,44 @@ +import random +import time +from typing import Callable + +import httpx + + +def retry[T](operations: list[Callable[[], T]], delay: float = 1.0) -> T: + """Retry an operation several times before failing.""" + for attempt, operation in enumerate(operations): + try: + return operation() + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + time.sleep(delay) + + +def fetch_backup_api() -> str: + """Fallback if the main API fails.""" + return "Backup API: Chuck Norris can delete the Recycle Bin." + + +def fetch_main_api() -> str: + """Fetch from the main Chuck Norris API.""" + # randomly raise an error to simulate failures + if random.random() < 0.8: + raise RuntimeError("simulated random failure") + with httpx.Client() as client: + response = client.get("https://api.chucknorris.io/jokes/random", timeout=2.0) + response.raise_for_status() + data: dict[str, str] = response.json() + return data["value"] + + +def get_joke() -> str: + return retry([fetch_main_api] * 3 + [fetch_backup_api]) + + +def main() -> None: + print(get_joke()) + + +if __name__ == "__main__": + main() diff --git a/2025/retry/7_tenacity.py b/2025/retry/8_tenacity.py similarity index 100% rename from 2025/retry/7_tenacity.py rename to 2025/retry/8_tenacity.py From 1094f221e428ac272262beefa8b9fc0f2f5277ee Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 5 Nov 2025 15:53:24 +0100 Subject: [PATCH 067/113] Added code example --- 2025/logic/1_starting_point.py | 88 ++++++++++++++++++ 2025/logic/2_characterization_tests.py | 88 ++++++++++++++++++ 2025/logic/3_guard_clauses.py | 79 ++++++++++++++++ 2025/logic/4_remove_try_except.py | 85 ++++++++++++++++++ 2025/logic/5_condition_naming.py | 85 ++++++++++++++++++ 2025/logic/6_simplify_loops.py | 84 +++++++++++++++++ 2025/logic/7_merge_logic.py | 83 +++++++++++++++++ 2025/logic/8_logic_to_data.py | 90 +++++++++++++++++++ 2025/logic/main.py | 90 +++++++++++++++++++ 2025/logic/pyproject.toml | 7 ++ 2025/logic/test_main.py | 119 +++++++++++++++++++++++++ 2025/logic/uv.lock | 75 ++++++++++++++++ 12 files changed, 973 insertions(+) create mode 100644 2025/logic/1_starting_point.py create mode 100644 2025/logic/2_characterization_tests.py create mode 100644 2025/logic/3_guard_clauses.py create mode 100644 2025/logic/4_remove_try_except.py create mode 100644 2025/logic/5_condition_naming.py create mode 100644 2025/logic/6_simplify_loops.py create mode 100644 2025/logic/7_merge_logic.py create mode 100644 2025/logic/8_logic_to_data.py create mode 100644 2025/logic/main.py create mode 100644 2025/logic/pyproject.toml create mode 100644 2025/logic/test_main.py create mode 100644 2025/logic/uv.lock diff --git a/2025/logic/1_starting_point.py b/2025/logic/1_starting_point.py new file mode 100644 index 00000000..6836bd38 --- /dev/null +++ b/2025/logic/1_starting_point.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def approve_order(order: Order, user: User) -> str: + """A tangled, messy function that weโ€™ll clean up in the video.""" + try: + if user.is_premium: + if order.amount > 1000: + if not order.has_discount: + if user.region != "EU": + for item in order.items: + if item.price < 0: + return "rejected" + return "approved" + else: + if order.currency == "EUR": + return "approved" + else: + return "rejected" + else: + return "rejected" + else: + if order.type == "bulk" and not user.is_trial: + return "approved" + else: + return "rejected" + else: + if user.is_admin: + return "approved" + else: + return "rejected" + except Exception: + # Just to be safe + return "rejected" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/2_characterization_tests.py b/2025/logic/2_characterization_tests.py new file mode 100644 index 00000000..6836bd38 --- /dev/null +++ b/2025/logic/2_characterization_tests.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def approve_order(order: Order, user: User) -> str: + """A tangled, messy function that weโ€™ll clean up in the video.""" + try: + if user.is_premium: + if order.amount > 1000: + if not order.has_discount: + if user.region != "EU": + for item in order.items: + if item.price < 0: + return "rejected" + return "approved" + else: + if order.currency == "EUR": + return "approved" + else: + return "rejected" + else: + return "rejected" + else: + if order.type == "bulk" and not user.is_trial: + return "approved" + else: + return "rejected" + else: + if user.is_admin: + return "approved" + else: + return "rejected" + except Exception: + # Just to be safe + return "rejected" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/3_guard_clauses.py b/2025/logic/3_guard_clauses.py new file mode 100644 index 00000000..bbb6e659 --- /dev/null +++ b/2025/logic/3_guard_clauses.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def approve_order(order: Order, user: User) -> str: + try: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + if not user.is_premium: + return "rejected" + if order.amount <= 1000 and order.type != "bulk" or user.is_trial: + return "rejected" + if order.has_discount: + return "rejected" + if user.region == "EU" and order.currency != "EUR": + return "rejected" + for item in order.items: + if item.price < 0: + return "rejected" + return "approved" + except Exception: + # Just to be safe + return "rejected" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/4_remove_try_except.py b/2025/logic/4_remove_try_except.py new file mode 100644 index 00000000..170e8d69 --- /dev/null +++ b/2025/logic/4_remove_try_except.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def has_valid_currency(order: Order, user: User) -> bool: + return not (user.region == "EU" and order.currency != "EUR") + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + if not user.is_premium: + return "rejected" + if order.amount is None: + return "rejected" + if order.amount <= 1000 and order.type != "bulk" or user.is_trial: + return "rejected" + if order.has_discount: + return "rejected" + if user.region == "EU" and order.currency != "EUR": + return "rejected" + for item in order.items: + if item.price < 0: + return "rejected" + return "approved" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/5_condition_naming.py b/2025/logic/5_condition_naming.py new file mode 100644 index 00000000..999f87ff --- /dev/null +++ b/2025/logic/5_condition_naming.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def has_valid_currency(order: Order, user: User) -> bool: + return not (user.region == "EU" and order.currency != "EUR") + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + if not user.is_premium: + return "rejected" + if order.amount is None: + return "rejected" + if not is_eligible_amount(order, user): + return "rejected" + if order.has_discount: + return "rejected" + if not has_valid_currency(order, user): + return "rejected" + for item in order.items: + if item.price < 0: + return "rejected" + return "approved" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/6_simplify_loops.py b/2025/logic/6_simplify_loops.py new file mode 100644 index 00000000..d614547c --- /dev/null +++ b/2025/logic/6_simplify_loops.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def has_valid_currency(order: Order, user: User) -> bool: + return not (user.region == "EU" and order.currency != "EUR") + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + if not user.is_premium: + return "rejected" + if order.amount is None: + return "rejected" + if not is_eligible_amount(order, user): + return "rejected" + if order.has_discount: + return "rejected" + if not has_valid_currency(order, user): + return "rejected" + if any(item.price < 0 for item in order.items): + return "rejected" + return "approved" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/7_merge_logic.py b/2025/logic/7_merge_logic.py new file mode 100644 index 00000000..9f80c5b5 --- /dev/null +++ b/2025/logic/7_merge_logic.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def has_valid_currency(order: Order, user: User) -> bool: + return not (user.region == "EU" and order.currency != "EUR") + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + rejection_rules = [ + lambda: not user.is_premium, + lambda: order.amount is None, + lambda: order.has_discount, + lambda: not is_eligible_amount(order, user), + lambda: not has_valid_currency(order, user), + lambda: any(item.price < 0 for item in order.items), + ] + + if any(rule() for rule in rejection_rules): + return "rejected" + return "approved" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/8_logic_to_data.py b/2025/logic/8_logic_to_data.py new file mode 100644 index 00000000..da53d0c5 --- /dev/null +++ b/2025/logic/8_logic_to_data.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +VALID_REGION_CURRENCY = { + ("EU", "EUR"): True, + ("US", "USD"): True, + ("UK", "GBP"): True, +} + + +def has_valid_currency(order: Order, user: User) -> bool: + return VALID_REGION_CURRENCY.get((user.region, order.currency), False) + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + rejection_rules = [ + lambda: not user.is_premium, + lambda: order.amount is None, + lambda: order.has_discount, + lambda: not is_eligible_amount(order, user), + lambda: not has_valid_currency(order, user), + lambda: any(item.price < 0 for item in order.items), + ] + + if any(rule() for rule in rejection_rules): + return "rejected" + return "approved" + + +def main() -> None: + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/main.py b/2025/logic/main.py new file mode 100644 index 00000000..44ea5ab4 --- /dev/null +++ b/2025/logic/main.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass, field + + +@dataclass +class Item: + name: str + price: float + + +@dataclass +class Order: + amount: float + has_discount: bool + region: str + currency: str + type: str # e.g. "bulk" or "normal" + items: list[Item] = field(default_factory=list) + + +@dataclass +class User: + is_premium: bool + is_admin: bool + is_trial: bool + region: str + + +VALID_REGION_CURRENCY = { + ("EU", "EUR"): True, + ("US", "USD"): True, + ("UK", "GBP"): True, +} + + +def has_valid_currency(order: Order, user: User) -> bool: + return VALID_REGION_CURRENCY.get((user.region, order.currency), False) + + +def is_eligible_amount(order: Order, user: User) -> bool: + return order.amount > 1000 or (order.type == "bulk" and not user.is_trial) + + +def approve_order(order: Order, user: User) -> str: + # 1) Privilege/override gate + if user.is_admin: + return "approved" + + # 2) Policy gate (rejections for non-admins) + rejection_rules = [ + lambda: not user.is_premium, + lambda: order.amount is None, + lambda: order.has_discount, + lambda: not is_eligible_amount(order, user), + lambda: not has_valid_currency(order, user), + lambda: any(item.price < 0 for item in order.items), + ] + + if any(rule() for rule in rejection_rules): + return "rejected" + return "approved" + + +def main(): + # Create a sample user and order that barely passes the approval rules + user = User( + is_premium=True, + is_admin=False, + is_trial=False, + region="US", + ) + + order = Order( + amount=1500, + has_discount=False, + region="EU", + currency="USD", + type="normal", + items=[ + Item("Keyboard", 100.0), + Item("Monitor", 200.0), + Item("Mouse", 50.0), + ], + ) + + result = approve_order(order, user) + print(f"Order approval result: {result}") + + +if __name__ == "__main__": + main() diff --git a/2025/logic/pyproject.toml b/2025/logic/pyproject.toml new file mode 100644 index 00000000..deaa9a36 --- /dev/null +++ b/2025/logic/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "logic" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "pytest>=8.4.2", +] diff --git a/2025/logic/test_main.py b/2025/logic/test_main.py new file mode 100644 index 00000000..7ed41ec3 --- /dev/null +++ b/2025/logic/test_main.py @@ -0,0 +1,119 @@ +from main import Item, Order, User, approve_order + + +def make_order(**kwargs): + """Helper to create an order with sane defaults.""" + defaults = dict( + amount=1200, + has_discount=False, + region="US", + currency="USD", + type="normal", + items=[Item("Keyboard", 100.0), Item("Mouse", 50.0)], + ) + defaults.update(kwargs) + return Order(**defaults) + + +def make_user(**kwargs): + """Helper to create a user with sane defaults.""" + defaults = dict( + is_premium=False, + is_admin=False, + is_trial=False, + region="US", + ) + defaults.update(kwargs) + return User(**defaults) + + +# --- Premium user scenarios --------------------------------------------------- + + +def test_premium_high_value_no_discount_non_eu(): + user = make_user(is_premium=True) + order = make_order(amount=1500, region="US", currency="USD", has_discount=False) + assert approve_order(order, user) == "approved" + + +def test_premium_high_value_no_discount_eu_eur(): + user = make_user(is_premium=True, region="EU") + order = make_order(amount=1500, region="EU", currency="EUR", has_discount=False) + assert approve_order(order, user) == "approved" + + +def test_premium_high_value_no_discount_eu_usd(): + user = make_user(is_premium=True, region="EU") + order = make_order(amount=1500, region="EU", currency="USD", has_discount=False) + assert approve_order(order, user) == "rejected" + + +def test_premium_high_value_with_discount(): + user = make_user(is_premium=True) + order = make_order(amount=1500, has_discount=True) + assert approve_order(order, user) == "rejected" + + +def test_premium_low_value_bulk_order_non_trial(): + user = make_user(is_premium=True, is_trial=False) + order = make_order(amount=500, type="bulk") + assert approve_order(order, user) == "approved" + + +def test_premium_low_value_bulk_trial_user(): + user = make_user(is_premium=True, is_trial=True) + order = make_order(amount=500, type="bulk") + assert approve_order(order, user) == "rejected" + + +def test_premium_low_value_normal_order(): + user = make_user(is_premium=True) + order = make_order(amount=500, type="normal") + assert approve_order(order, user) == "rejected" + + +def test_premium_high_value_negative_item_price(): + user = make_user(is_premium=True) + order = make_order(amount=1500, items=[Item("Broken item", -10)]) + assert approve_order(order, user) == "rejected" + + +# --- Admin user scenarios ----------------------------------------------------- + + +def test_admin_user_approved_regardless(): + user = make_user(is_admin=True) + order = make_order(amount=100, has_discount=True) + assert approve_order(order, user) == "approved" + + +def test_admin_user_with_negative_item(): + user = make_user(is_admin=True) + order = make_order(items=[Item("Something", -5)]) + # still approved, because admin bypasses everything + assert approve_order(order, user) == "approved" + + +# --- Regular (non-premium, non-admin) user scenarios -------------------------- + + +def test_regular_user_rejected_even_if_high_value(): + user = make_user() + order = make_order(amount=2000) + assert approve_order(order, user) == "rejected" + + +def test_regular_user_with_discount(): + user = make_user() + order = make_order(has_discount=True) + assert approve_order(order, user) == "rejected" + + +# --- Exception handling ------------------------------------------------------- + + +def test_invalid_order_raises_exception_is_caught(): + user = make_user(is_premium=True) + # Pass a broken order missing 'amount' field to trigger an exception + broken_order = make_order(amount=None) + assert approve_order(broken_order, user) == "rejected" diff --git a/2025/logic/uv.lock b/2025/logic/uv.lock new file mode 100644 index 00000000..8d1c2e5d --- /dev/null +++ b/2025/logic/uv.lock @@ -0,0 +1,75 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "logic" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "pytest", specifier = ">=8.4.2" }] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] From c2a9cd4e827b1e3eefd82d5260b1a71e0c590621 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 27 Nov 2025 15:58:59 +0100 Subject: [PATCH 068/113] Added code example --- 2026/spec/main.py | 120 ++++++++++++++++++++++++++++++++++++ 2026/spec/messy.py | 105 +++++++++++++++++++++++++++++++ 2026/spec/pyproject.toml | 7 +++ 2026/spec/rule_config.json | 29 +++++++++ 2026/spec/rules.py | 122 +++++++++++++++++++++++++++++++++++++ 2026/spec/uv.lock | 8 +++ 6 files changed, 391 insertions(+) create mode 100644 2026/spec/main.py create mode 100644 2026/spec/messy.py create mode 100644 2026/spec/pyproject.toml create mode 100644 2026/spec/rule_config.json create mode 100644 2026/spec/rules.py create mode 100644 2026/spec/uv.lock diff --git a/2026/spec/main.py b/2026/spec/main.py new file mode 100644 index 00000000..62a7d396 --- /dev/null +++ b/2026/spec/main.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +from typing import Iterable + +from rules import RuleFn, load_rule_from_config, rule + +# --------------------------------------------------------------------------- +# DOMAIN MODEL +# --------------------------------------------------------------------------- + + +@dataclass +class User: + is_admin: bool + is_active: bool + account_age: int + is_banned: bool + country: str + credit_score: int + has_manual_override: bool + + +# --------------------------------------------------------------------------- +# BUSINESS RULES (SIMPLY USE @predicate) +# --------------------------------------------------------------------------- + + +@rule +def is_admin() -> RuleFn[User]: + return lambda u: u.is_admin + + +@rule +def is_active() -> RuleFn[User]: + return lambda u: u.is_active + + +@rule +def is_banned() -> RuleFn[User]: + return lambda u: u.is_banned + + +@rule +def has_override() -> RuleFn[User]: + return lambda u: u.has_manual_override + + +@rule +def account_older_than(days: int) -> RuleFn[User]: + return lambda u: u.account_age > days + + +@rule +def from_country(countries: Iterable[str]) -> RuleFn[User]: + return lambda u: u.country in countries + + +@rule +def credit_score_above(threshold: int) -> RuleFn[User]: + return lambda u: u.credit_score > threshold + + +# --------------------------------------------------------------------------- +# BUILD RULE IN PYTHON DSL +# --------------------------------------------------------------------------- + +AccessRule = is_admin() | ( + is_active() + & account_older_than(30) + & ~is_banned() + & from_country(["NL", "BE"]) + & (credit_score_above(650) | has_override()) +) + +# --------------------------------------------------------------------------- +# EXAMPLE SYSTEM USAGE +# --------------------------------------------------------------------------- + + +def api_check(user: User): + return AccessRule(user) + + +def reporting(users: list[User]) -> list[User]: + return [u for u in users if AccessRule(u)] + + +def cli_export(users: list[User]) -> list[User]: + return [u for u in users if AccessRule(u)] + + +# --------------------------------------------------------------------------- +# DEMO +# --------------------------------------------------------------------------- + + +def main() -> None: + users = [ + User(True, False, 1, False, "US", 100, False), + User(False, True, 40, False, "NL", 700, False), + User(False, True, 40, False, "BE", 500, True), + User(False, True, 5, False, "NL", 900, False), + User(False, False, 100, True, "NL", 900, False), + ] + + print("=== Access via Python DSL ===") + for u in users: + print(u, "=>", api_check(u)) + + # If rule_config.json exists, load dynamic rule: + try: + DynamicRule = load_rule_from_config("rule_config.json") + print("\n=== Access via Config Rule ===") + for u in users: + print(u, "=>", DynamicRule(u)) + except FileNotFoundError: + print("\n(No rule_config.json found โ€” skipping dynamic demo)") + + +if __name__ == "__main__": + main() diff --git a/2026/spec/messy.py b/2026/spec/messy.py new file mode 100644 index 00000000..c9c81d1c --- /dev/null +++ b/2026/spec/messy.py @@ -0,0 +1,105 @@ +""" +BEFORE version โ€” messy, duplicated, inconsistent business rules. + +We pretend that three different parts of the system +(API endpoint, report builder, CLI export) all need to check +whether a user has access to a premium feature. + +Each part implements the rule slightly differently. +""" + +from dataclasses import dataclass + + +@dataclass +class User: + id: int + is_admin: bool + is_active: bool + account_age: int + is_banned: bool + country: str + credit_score: int + has_manual_override: bool + + +# ------------------------------------------------------------------------- +# API CHECK โ€” deep nesting, hard to follow +# ------------------------------------------------------------------------- + + +def api_can_access(user: User) -> bool: + if user.is_admin: + return True + + if user.is_active and user.account_age > 30: + if not user.is_banned: + if user.country in {"NL", "BE"}: + if user.credit_score > 650 or user.has_manual_override: + return True + + return False + + +# ------------------------------------------------------------------------- +# REPORT BUILDER โ€” duplicated logic, but slightly different +# (forgot to check country; used >= instead of >) +# ------------------------------------------------------------------------- + + +def report_can_access(user: User) -> bool: + if user.is_admin: + return True + + if user.is_active: + if user.account_age >= 30: # subtle bug: >= vs > + if not user.is_banned: + # forgot country check entirely + if user.credit_score > 650 or user.has_manual_override: + return True + + return False + + +# ------------------------------------------------------------------------- +# CLI EXPORT โ€” another version, because of course +# (forgot override check; added "DE" because someone assumed EU == OK) +# ------------------------------------------------------------------------- + + +def cli_can_access(user: User) -> bool: + if user.is_admin: + return True + + if user.is_active and user.account_age > 30 and not user.is_banned: + if user.country in {"NL", "BE", "DE"}: # accidental extra country + if user.credit_score > 650: # forgot override + return True + + return False + + +# ------------------------------------------------------------------------- +# Demo: show discrepancies +# ------------------------------------------------------------------------- + + +def main() -> None: + user = User( + id=1, + is_admin=False, + is_active=True, + account_age=35, + is_banned=False, + country="NL", + credit_score=600, + has_manual_override=True, + ) + + print("API says: ", api_can_access(user)) + print("Report says: ", report_can_access(user)) + print("CLI says: ", cli_can_access(user)) + + +if __name__ == "__main__": + main() diff --git a/2026/spec/pyproject.toml b/2026/spec/pyproject.toml new file mode 100644 index 00000000..fd2dd2da --- /dev/null +++ b/2026/spec/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "pythonic" +version = "0.1.0" +description = "Example of refactoring to be more Pythonic" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/spec/rule_config.json b/2026/spec/rule_config.json new file mode 100644 index 00000000..6047b754 --- /dev/null +++ b/2026/spec/rule_config.json @@ -0,0 +1,29 @@ +{ + "logic": "AND", + "conditions": [ + { + "name": "is_active" + }, + { + "name": "account_older_than", + "args": [ + 30 + ] + }, + { + "name": "from_country", + "args": [ + [ + "NL", + "BE" + ] + ] + }, + { + "name": "credit_score_above", + "args": [ + 650 + ] + } + ] +} \ No newline at end of file diff --git a/2026/spec/rules.py b/2026/spec/rules.py new file mode 100644 index 00000000..d3530396 --- /dev/null +++ b/2026/spec/rules.py @@ -0,0 +1,122 @@ +import json +from functools import wraps +from typing import Any, Callable + +# ------------------------------------------------------------ +# Generic Types +# ------------------------------------------------------------ + +type RuleFn[T] = Callable[[T], bool] +type RuleFactory[T] = Callable[..., RuleFn[T]] +type PredicateFactory[T] = Callable[..., Predicate[T]] + + +# ------------------------------------------------------------ +# Global Rule Registry +# ------------------------------------------------------------ + +RULES: dict[str, PredicateFactory[Any]] = {} + + +# ------------------------------------------------------------ +# Predicate +# ------------------------------------------------------------ + + +class Predicate[T]: + """ + A composable predicate that supports &, |, and ~ operators. + Wraps a function (T -> bool). + """ + + def __init__(self, fn: RuleFn[T]): + self.fn = fn + + def __call__(self, obj: T) -> bool: + return self.fn(obj) + + def __and__(self, other: Predicate[T]) -> Predicate[T]: + return Predicate(lambda x: self(x) and other(x)) + + def __or__(self, other: Predicate[T]) -> Predicate[T]: + return Predicate(lambda x: self(x) or other(x)) + + def __invert__(self) -> Predicate[T]: + return Predicate(lambda x: not self(x)) + + +# ------------------------------------------------------------ +# Decorators +# ------------------------------------------------------------ + + +def predicate[T](fn: RuleFn[T]) -> Predicate[T]: + """ + Wrap a simple function(obj) -> bool into a Predicate[T]. + This is used *inside* rule factories when building actual predicates. + """ + + @wraps(fn) + def wrapper(obj: T) -> bool: + return fn(obj) + + return Predicate(wrapper) + + +def rule[T](fn: RuleFactory[T]) -> PredicateFactory[T]: + """ + Decorator for rule factories. + """ + + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Predicate[T]: + result = fn(*args, **kwargs) + return Predicate(result) + + RULES[fn.__name__] = wrapper + return wrapper + + +# ------------------------------------------------------------ +# Config Loader +# ------------------------------------------------------------ + + +def load_rule_from_config(path: str) -> Predicate[Any]: + """ + Load a rule from a JSON config file that looks like: + + { + "logic": "AND", + "conditions": [ + {"name": "is_active", "args": []}, + {"name": "older_than", "args": [30]} + ] + } + + The returned object is a composed Predicate[Any]. + """ + + with open(path) as f: + config = json.load(f) + + preds: list[Predicate[Any]] = [] + + for cond in config["conditions"]: + name = cond["name"] + args = cond.get("args", []) + + if name not in RULES: + raise ValueError(f"Unknown rule: {name}") + + factory = RULES[name] + predicate_obj = factory(*args) + preds.append(predicate_obj) + + combined = preds[0] + logic = config["logic"] + + for p in preds[1:]: + combined = (combined & p) if logic == "AND" else (combined | p) + + return combined diff --git a/2026/spec/uv.lock b/2026/spec/uv.lock new file mode 100644 index 00000000..ae75f671 --- /dev/null +++ b/2026/spec/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "pythonic" +version = "0.1.0" +source = { virtual = "." } From 78ac3727b568c918625c3eaa2e3c29962fee3bd1 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 10 Dec 2025 17:01:13 +0100 Subject: [PATCH 069/113] Minor tweak to simplify rule system. --- 2026/spec/main.py | 42 ++++++++++++++++++++---------------------- 2026/spec/rules.py | 22 ++++++++++++++++++---- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/2026/spec/main.py b/2026/spec/main.py index 62a7d396..cd8312e8 100644 --- a/2026/spec/main.py +++ b/2026/spec/main.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Iterable -from rules import RuleFn, load_rule_from_config, rule +from rules import load_rule_from_config, rule # --------------------------------------------------------------------------- # DOMAIN MODEL @@ -25,45 +25,45 @@ class User: @rule -def is_admin() -> RuleFn[User]: - return lambda u: u.is_admin +def is_admin(u: User) -> bool: + return u.is_admin @rule -def is_active() -> RuleFn[User]: - return lambda u: u.is_active +def is_active(u: User) -> bool: + return u.is_active @rule -def is_banned() -> RuleFn[User]: - return lambda u: u.is_banned +def is_banned(u: User) -> bool: + return u.is_banned @rule -def has_override() -> RuleFn[User]: - return lambda u: u.has_manual_override +def has_override(u: User) -> bool: + return u.has_manual_override @rule -def account_older_than(days: int) -> RuleFn[User]: - return lambda u: u.account_age > days +def account_older_than(days: int, u: User) -> bool: + return u.account_age > days @rule -def from_country(countries: Iterable[str]) -> RuleFn[User]: - return lambda u: u.country in countries +def from_country(countries: Iterable[str], u: User) -> bool: + return u.country in countries @rule -def credit_score_above(threshold: int) -> RuleFn[User]: - return lambda u: u.credit_score > threshold +def credit_score_above(threshold: int, u: User) -> bool: + return u.credit_score > threshold # --------------------------------------------------------------------------- # BUILD RULE IN PYTHON DSL # --------------------------------------------------------------------------- -AccessRule = is_admin() | ( +api_check = is_admin() | ( is_active() & account_older_than(30) & ~is_banned() @@ -76,16 +76,14 @@ def credit_score_above(threshold: int) -> RuleFn[User]: # --------------------------------------------------------------------------- -def api_check(user: User): - return AccessRule(user) def reporting(users: list[User]) -> list[User]: - return [u for u in users if AccessRule(u)] + return [u for u in users if api_check(u)] def cli_export(users: list[User]) -> list[User]: - return [u for u in users if AccessRule(u)] + return [u for u in users if api_check(u)] # --------------------------------------------------------------------------- @@ -108,10 +106,10 @@ def main() -> None: # If rule_config.json exists, load dynamic rule: try: - DynamicRule = load_rule_from_config("rule_config.json") + dynamic_rule = load_rule_from_config("rule_config.json") print("\n=== Access via Config Rule ===") for u in users: - print(u, "=>", DynamicRule(u)) + print(u, "=>", dynamic_rule(u)) except FileNotFoundError: print("\n(No rule_config.json found โ€” skipping dynamic demo)") diff --git a/2026/spec/rules.py b/2026/spec/rules.py index d3530396..6d3251a7 100644 --- a/2026/spec/rules.py +++ b/2026/spec/rules.py @@ -7,7 +7,7 @@ # ------------------------------------------------------------ type RuleFn[T] = Callable[[T], bool] -type RuleFactory[T] = Callable[..., RuleFn[T]] +type RuleFactory[T] = Callable[..., bool] type PredicateFactory[T] = Callable[..., Predicate[T]] @@ -63,15 +63,29 @@ def wrapper(obj: T) -> bool: return Predicate(wrapper) -def rule[T](fn: RuleFactory[T]) -> PredicateFactory[T]: + +def rule[T](fn: RuleFactory[T]) -> PredicateFactory[Any]: """ Decorator for rule factories. + + A rule factory looks like: + + @rule + def account_older_than(days, u): + return u.account_age > days + + The function MAY take parameters, but must always + take the domain object 'u' as its final argument. + + The decorator automatically: + - partially applies parameters + - wraps the result into a Predicate[T] + - registers the rule for config-based lookup """ @wraps(fn) def wrapper(*args: Any, **kwargs: Any) -> Predicate[T]: - result = fn(*args, **kwargs) - return Predicate(result) + return Predicate(lambda obj: fn(*args, obj, **kwargs)) RULES[fn.__name__] = wrapper return wrapper From 76234c150c016558536c3f07c4cbf983a544c24f Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 10 Dec 2025 17:06:03 +0100 Subject: [PATCH 070/113] Updated rule factory type definition. --- 2026/spec/rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2026/spec/rules.py b/2026/spec/rules.py index 6d3251a7..2203ffb1 100644 --- a/2026/spec/rules.py +++ b/2026/spec/rules.py @@ -7,7 +7,7 @@ # ------------------------------------------------------------ type RuleFn[T] = Callable[[T], bool] -type RuleFactory[T] = Callable[..., bool] +type RuleFactory = Callable[..., bool] type PredicateFactory[T] = Callable[..., Predicate[T]] @@ -64,7 +64,7 @@ def wrapper(obj: T) -> bool: -def rule[T](fn: RuleFactory[T]) -> PredicateFactory[Any]: +def rule[T](fn: RuleFactory) -> PredicateFactory[Any]: """ Decorator for rule factories. From bdd0163f50984f041655b06068fee1b2c9f6aa99 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 11 Dec 2025 16:22:21 +0100 Subject: [PATCH 071/113] Minor fixes in rules example. --- 2026/spec/rules.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/2026/spec/rules.py b/2026/spec/rules.py index 2203ffb1..922e344c 100644 --- a/2026/spec/rules.py +++ b/2026/spec/rules.py @@ -6,8 +6,8 @@ # Generic Types # ------------------------------------------------------------ -type RuleFn[T] = Callable[[T], bool] -type RuleFactory = Callable[..., bool] +type PredicateFn[T] = Callable[[T], bool] +type RuleDef = Callable[..., bool] type PredicateFactory[T] = Callable[..., Predicate[T]] @@ -29,7 +29,7 @@ class Predicate[T]: Wraps a function (T -> bool). """ - def __init__(self, fn: RuleFn[T]): + def __init__(self, fn: PredicateFn[T]): self.fn = fn def __call__(self, obj: T) -> bool: @@ -50,12 +50,7 @@ def __invert__(self) -> Predicate[T]: # ------------------------------------------------------------ -def predicate[T](fn: RuleFn[T]) -> Predicate[T]: - """ - Wrap a simple function(obj) -> bool into a Predicate[T]. - This is used *inside* rule factories when building actual predicates. - """ - +def predicate[T](fn: PredicateFn[T]) -> Predicate[T]: @wraps(fn) def wrapper(obj: T) -> bool: return fn(obj) @@ -64,25 +59,7 @@ def wrapper(obj: T) -> bool: -def rule[T](fn: RuleFactory) -> PredicateFactory[Any]: - """ - Decorator for rule factories. - - A rule factory looks like: - - @rule - def account_older_than(days, u): - return u.account_age > days - - The function MAY take parameters, but must always - take the domain object 'u' as its final argument. - - The decorator automatically: - - partially applies parameters - - wraps the result into a Predicate[T] - - registers the rule for config-based lookup - """ - +def rule[T](fn: RuleDef) -> PredicateFactory[Any]: @wraps(fn) def wrapper(*args: Any, **kwargs: Any) -> Predicate[T]: return Predicate(lambda obj: fn(*args, obj, **kwargs)) From be8ab7e97904f78de6b9cbb03dad9b04523367e0 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 3 Dec 2025 16:32:38 +0100 Subject: [PATCH 072/113] Added code example. --- 2026/fluent/animation.py | 147 ++++++++++++++++++++++++++++++++++ 2026/fluent/graphics.py | 25 ++++++ 2026/fluent/main.py | 119 +++++++++++++++++++++++++++ 2026/fluent/pyproject.toml | 6 ++ 2026/fluent/starting_point.py | 40 +++++++++ 2026/fluent/uv.lock | 8 ++ 6 files changed, 345 insertions(+) create mode 100644 2026/fluent/animation.py create mode 100644 2026/fluent/graphics.py create mode 100644 2026/fluent/main.py create mode 100644 2026/fluent/pyproject.toml create mode 100644 2026/fluent/starting_point.py create mode 100644 2026/fluent/uv.lock diff --git a/2026/fluent/animation.py b/2026/fluent/animation.py new file mode 100644 index 00000000..02524d8c --- /dev/null +++ b/2026/fluent/animation.py @@ -0,0 +1,147 @@ +import math +from dataclasses import dataclass, field +from typing import Protocol, Self +from graphics import ShapeData, Color + + +# ------------------------------------------------------------ +# Animation Step Protocol +# ------------------------------------------------------------ + + +class AnimationStep(Protocol): + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + """ + t โˆˆ [0, 1] โ€” the local progress through this step. + """ + ... + + +# ------------------------------------------------------------ +# Helper +# ------------------------------------------------------------ + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +# ------------------------------------------------------------ +# Concrete Steps (no duration stored here) +# ------------------------------------------------------------ + + +@dataclass +class Move: + dx: float + dy: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + new_points = [(x + self.dx * t, y + self.dy * t) for (x, y) in shape] + return new_points, color + + +@dataclass +class Rotate: + angle: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + theta = math.radians(self.angle * t) + cx = sum(x for x, _ in shape) / len(shape) + cy = sum(y for _, y in shape) / len(shape) + + out: ShapeData = [] + for x, y in shape: + nx = cx + (x - cx) * math.cos(theta) - (y - cy) * math.sin(theta) + ny = cy + (x - cx) * math.sin(theta) + (y - cy) * math.cos(theta) + out.append((nx, ny)) + + return out, color + + +@dataclass +class Scale: + factor: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + cx = sum(x for x, _ in shape) / len(shape) + cy = sum(y for _, y in shape) / len(shape) + + s = lerp(1.0, self.factor, t) + new_points = [(cx + (x - cx) * s, cy + (y - cy) * s) for (x, y) in shape] + + return new_points, color + + +@dataclass +class Fade: + brightness: int # target grayscale (0โ€“255) + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + current_val = int(color[1:3], 16) + new_val = int(lerp(current_val, self.brightness, t)) + new_color = f"#{new_val:02x}{new_val:02x}{new_val:02x}" + return shape, new_color + + +# ------------------------------------------------------------ +# Animation Timeline (duration now stored here) +# ------------------------------------------------------------ + + +@dataclass +class Animation: + steps: list[AnimationStep] = field(default_factory=list[AnimationStep]) + durations: list[float] = field( + default_factory=list[float] + ) + start_time: float = 0.0 + + def add(self, step: AnimationStep, duration: float) -> Self: + self.steps.append(step) + self.durations.append(duration) + return self + + # fluent API + def move(self, dx: float, dy: float, duration: float = 0.5) -> Self: + return self.add(Move(dx, dy), duration) + + def rotate(self, angle: float, duration: float = 1.0) -> Self: + return self.add(Rotate(angle), duration) + + def scale(self, factor: float, duration: float = 0.5) -> Self: + return self.add(Scale(factor), duration) + + def fade_to(self, brightness: int, duration: float = 0.5) -> Self: + return self.add(Fade(brightness), duration) + + @property + def duration(self) -> float: + return sum(self.durations) + + @property + def end_time(self) -> float: + return self.start_time + self.duration + + +# ------------------------------------------------------------ +# Shape with attached animation +# ------------------------------------------------------------ + + +@dataclass +class Shape: + shape_id: str + points: ShapeData + color: Color = "#444444" + animation: Animation | None = None diff --git a/2026/fluent/graphics.py b/2026/fluent/graphics.py new file mode 100644 index 00000000..23f04a18 --- /dev/null +++ b/2026/fluent/graphics.py @@ -0,0 +1,25 @@ +import tkinter as tk +from itertools import chain + +Point = tuple[float, float] +ShapeData = list[Point] +Color = str + + +class GraphicsRenderer: + def __init__(self, canvas: tk.Canvas): + self.canvas = canvas + self.items: dict[str, int] = {} + + def render(self, shape_id: str, points: ShapeData, color: Color) -> None: + flat = list(chain.from_iterable(points)) + if shape_id not in self.items: + self.items[shape_id] = self.canvas.create_polygon( + flat, fill=color, outline=color + ) + else: + item = self.items[shape_id] + self.canvas.coords(item, *flat) + self.canvas.itemconfig(item, fill=color, outline=color) + + self.canvas.update() diff --git a/2026/fluent/main.py b/2026/fluent/main.py new file mode 100644 index 00000000..dec6e16a --- /dev/null +++ b/2026/fluent/main.py @@ -0,0 +1,119 @@ + + +import time +import tkinter as tk +from typing import Any + +from graphics import GraphicsRenderer +from animation import Shape, Animation + + +def play_scene(renderer: GraphicsRenderer, shapes: list[Shape]) -> None: + animations = [s.animation for s in shapes if s.animation is not None] + if not animations: + return + + + global_end = max(anim.end_time for anim in animations) + t0 = time.time() + + while True: + now = time.time() - t0 + finished = True + + for shape in shapes: + anim = shape.animation + if anim is None: + continue + + # Active? + if not (anim.start_time <= now <= anim.end_time): + continue + + finished = False + + # Time inside this animation + t_anim = now - anim.start_time + time_cursor = 0.0 + + # Always start computations from the original state + points = shape.points + color = shape.color + + for step, duration in zip(anim.steps, anim.durations): + if duration <= 0: + continue + + local_t_raw = (t_anim - time_cursor) / duration + local_t = max(0.0, min(1.0, local_t_raw)) + + points, color = step.apply(points, color, local_t) + + # If we are still inside this step, it means this is the active one โ†’ stop + if local_t < 1.0: + break + + time_cursor += duration + + renderer.render(shape.shape_id, points, color) + + renderer.canvas.update() + + if finished and now >= global_end: + break + + time.sleep(0.01) + +def main() -> None: + root = tk.Tk() + root.title("ShapyMcShapeface Renderer") + + canvas_width = 800 + canvas_height = 600 + + canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white") + canvas.pack() + + renderer = GraphicsRenderer(canvas) + + square = Shape( + shape_id="square", + points=[(250, 200), (350, 200), (350, 300), (250, 300)], + color="#444444", + animation=( + Animation(start_time=0.0) + .rotate(60, duration=1.0) + .move(200, 0, duration=1.0) + .scale(1.3, duration=1.0) + .fade_to(200, duration=0.8) + .move(0, 120, duration=1.0) + .fade_to(40, duration=0.8) + ), + ) + + triangle = Shape( + shape_id="triangle", + points=[(500, 250), (550, 350), (450, 350)], + color="#888888", + animation=( + Animation(start_time=0.5) + .fade_to(220, duration=1.0) + .move(-80, 40, duration=1.0) + .scale(0.9, duration=1.0) + ), + ) + + shapes = [square, triangle] + + def start_animation(event: Any): + play_scene(renderer, shapes) + + # Start on click or keypress + canvas.bind("", start_animation) + root.bind("", start_animation) + + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2026/fluent/pyproject.toml b/2026/fluent/pyproject.toml new file mode 100644 index 00000000..b2573448 --- /dev/null +++ b/2026/fluent/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "fluent" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/fluent/starting_point.py b/2026/fluent/starting_point.py new file mode 100644 index 00000000..4e7a4389 --- /dev/null +++ b/2026/fluent/starting_point.py @@ -0,0 +1,40 @@ + +import tkinter as tk + +from graphics import GraphicsRenderer, ShapeData, Color + + +def main() -> None: + root = tk.Tk() + root.title("ShapyMcShapeface Renderer") + + canvas_width = 800 + canvas_height = 600 + + canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white") + canvas.pack() + + renderer = GraphicsRenderer(canvas) + + # ------------------------------------------------------------ + # Define a few shapes + # ------------------------------------------------------------ + + square_points: ShapeData = [(250, 200), (350, 200), (350, 300), (250, 300)] + square_color: Color = "#444444" + + triangle_points: ShapeData = [(500, 250), (550, 350), (450, 350)] + triangle_color: Color = "#888888" + + # ------------------------------------------------------------ + # Render shapes using the renderer + # ------------------------------------------------------------ + + renderer.render("square", square_points, square_color) + renderer.render("triangle", triangle_points, triangle_color) + + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2026/fluent/uv.lock b/2026/fluent/uv.lock new file mode 100644 index 00000000..8d1505a3 --- /dev/null +++ b/2026/fluent/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "fluent" +version = "0.0.1" +source = { virtual = "." } From 8010cd9251ff05605ed3a0ff10b0036a43ce0729 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 9 Dec 2025 16:51:45 +0100 Subject: [PATCH 073/113] Various fixes in code example. Added proper before version. --- 2026/fluent/animation.py | 59 +++++++++- 2026/fluent/animation_before.py | 186 ++++++++++++++++++++++++++++++++ 2026/fluent/main.py | 59 +--------- 2026/fluent/main_before.py | 93 ++++++++++++++++ 2026/fluent/starting_point.py | 40 ------- 5 files changed, 338 insertions(+), 99 deletions(-) create mode 100644 2026/fluent/animation_before.py create mode 100644 2026/fluent/main_before.py delete mode 100644 2026/fluent/starting_point.py diff --git a/2026/fluent/animation.py b/2026/fluent/animation.py index 02524d8c..54fc3d91 100644 --- a/2026/fluent/animation.py +++ b/2026/fluent/animation.py @@ -1,7 +1,8 @@ import math from dataclasses import dataclass, field from typing import Protocol, Self -from graphics import ShapeData, Color +from graphics import ShapeData, Color, GraphicsRenderer +import time # ------------------------------------------------------------ @@ -145,3 +146,59 @@ class Shape: points: ShapeData color: Color = "#444444" animation: Animation | None = None + +def play_scene(renderer: GraphicsRenderer, shapes: list[Shape]) -> None: + animations = [s.animation for s in shapes if s.animation is not None] + if not animations: + return + + + global_end = max(anim.end_time for anim in animations) + t0 = time.time() + + while True: + now = time.time() - t0 + finished = True + + for shape in shapes: + anim = shape.animation + if anim is None: + continue + + # Active? + if not (anim.start_time <= now <= anim.end_time): + continue + + finished = False + + # Time inside this animation + t_anim = now - anim.start_time + time_cursor = 0.0 + + # Always start computations from the original state + points = shape.points + color = shape.color + + for step, duration in zip(anim.steps, anim.durations): + if duration <= 0: + continue + + local_t_raw = (t_anim - time_cursor) / duration + local_t = max(0.0, min(1.0, local_t_raw)) + + points, color = step.apply(points, color, local_t) + + # If we are still inside this step, it means this is the active one โ†’ stop + if local_t < 1.0: + break + + time_cursor += duration + + renderer.render(shape.shape_id, points, color) + + # renderer.canvas.update() + + if finished and now >= global_end: + break + + time.sleep(0.01) diff --git a/2026/fluent/animation_before.py b/2026/fluent/animation_before.py new file mode 100644 index 00000000..cbeae9ee --- /dev/null +++ b/2026/fluent/animation_before.py @@ -0,0 +1,186 @@ +import math +from dataclasses import dataclass, field +from typing import Protocol +from graphics import ShapeData, Color, GraphicsRenderer +import time + + +# ------------------------------------------------------------ +# Animation Step Protocol +# ------------------------------------------------------------ + + +class AnimationStep(Protocol): + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + """ + t โˆˆ [0, 1] โ€” the local progress through this step. + """ + ... + + +# ------------------------------------------------------------ +# Helper +# ------------------------------------------------------------ + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +# ------------------------------------------------------------ +# Concrete Steps (no duration stored here) +# ------------------------------------------------------------ + + +@dataclass +class Move: + dx: float + dy: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + new_points = [(x + self.dx * t, y + self.dy * t) for (x, y) in shape] + return new_points, color + + +@dataclass +class Rotate: + angle: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + theta = math.radians(self.angle * t) + cx = sum(x for x, _ in shape) / len(shape) + cy = sum(y for _, y in shape) / len(shape) + + out: ShapeData = [] + for x, y in shape: + nx = cx + (x - cx) * math.cos(theta) - (y - cy) * math.sin(theta) + ny = cy + (x - cx) * math.sin(theta) + (y - cy) * math.cos(theta) + out.append((nx, ny)) + + return out, color + + +@dataclass +class Scale: + factor: float + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + cx = sum(x for x, _ in shape) / len(shape) + cy = sum(y for _, y in shape) / len(shape) + + s = lerp(1.0, self.factor, t) + new_points = [(cx + (x - cx) * s, cy + (y - cy) * s) for (x, y) in shape] + + return new_points, color + + +@dataclass +class Fade: + brightness: int # target grayscale (0โ€“255) + + def apply( + self, shape: ShapeData, color: Color, t: float + ) -> tuple[ShapeData, Color]: + current_val = int(color[1:3], 16) + new_val = int(lerp(current_val, self.brightness, t)) + new_color = f"#{new_val:02x}{new_val:02x}{new_val:02x}" + return shape, new_color + + +# ------------------------------------------------------------ +# Animation Timeline (duration now stored here) +# ------------------------------------------------------------ + + +@dataclass +class Animation: + steps: list[AnimationStep] = field(default_factory=list[AnimationStep]) + durations: list[float] = field( + default_factory=list[float] + ) + start_time: float = 0.0 + + @property + def duration(self) -> float: + return sum(self.durations) + + @property + def end_time(self) -> float: + return self.start_time + self.duration + + +# ------------------------------------------------------------ +# Shape with attached animation +# ------------------------------------------------------------ + + +@dataclass +class Shape: + shape_id: str + points: ShapeData + color: Color = "#444444" + animation: Animation | None = None + +def play_scene(renderer: GraphicsRenderer, shapes: list[Shape]) -> None: + animations = [s.animation for s in shapes if s.animation is not None] + if not animations: + return + + + global_end = max(anim.end_time for anim in animations) + t0 = time.time() + + while True: + now = time.time() - t0 + finished = True + + for shape in shapes: + anim = shape.animation + if anim is None: + continue + + # Active? + if not (anim.start_time <= now <= anim.end_time): + continue + + finished = False + + # Time inside this animation + t_anim = now - anim.start_time + time_cursor = 0.0 + + # Always start computations from the original state + points = shape.points + color = shape.color + + for step, duration in zip(anim.steps, anim.durations): + if duration <= 0: + continue + + local_t_raw = (t_anim - time_cursor) / duration + local_t = max(0.0, min(1.0, local_t_raw)) + + points, color = step.apply(points, color, local_t) + + # If we are still inside this step, it means this is the active one โ†’ stop + if local_t < 1.0: + break + + time_cursor += duration + + renderer.render(shape.shape_id, points, color) + + # renderer.canvas.update() + + if finished and now >= global_end: + break + + time.sleep(0.01) diff --git a/2026/fluent/main.py b/2026/fluent/main.py index dec6e16a..18a9b716 100644 --- a/2026/fluent/main.py +++ b/2026/fluent/main.py @@ -1,68 +1,11 @@ - - -import time import tkinter as tk from typing import Any from graphics import GraphicsRenderer -from animation import Shape, Animation - - -def play_scene(renderer: GraphicsRenderer, shapes: list[Shape]) -> None: - animations = [s.animation for s in shapes if s.animation is not None] - if not animations: - return - - - global_end = max(anim.end_time for anim in animations) - t0 = time.time() - - while True: - now = time.time() - t0 - finished = True - - for shape in shapes: - anim = shape.animation - if anim is None: - continue - - # Active? - if not (anim.start_time <= now <= anim.end_time): - continue - - finished = False - - # Time inside this animation - t_anim = now - anim.start_time - time_cursor = 0.0 - - # Always start computations from the original state - points = shape.points - color = shape.color - - for step, duration in zip(anim.steps, anim.durations): - if duration <= 0: - continue - - local_t_raw = (t_anim - time_cursor) / duration - local_t = max(0.0, min(1.0, local_t_raw)) - - points, color = step.apply(points, color, local_t) - - # If we are still inside this step, it means this is the active one โ†’ stop - if local_t < 1.0: - break - - time_cursor += duration - - renderer.render(shape.shape_id, points, color) +from animation import Shape, Animation, play_scene - renderer.canvas.update() - if finished and now >= global_end: - break - time.sleep(0.01) def main() -> None: root = tk.Tk() diff --git a/2026/fluent/main_before.py b/2026/fluent/main_before.py new file mode 100644 index 00000000..d74584c2 --- /dev/null +++ b/2026/fluent/main_before.py @@ -0,0 +1,93 @@ +import tkinter as tk +from typing import Any + +from graphics import GraphicsRenderer +from animation_before import Shape, Animation, play_scene, Rotate, Move, Scale, Fade + + + + +def main() -> None: + root = tk.Tk() + root.title("ShapyMcShapeface Renderer") + + canvas_width = 800 + canvas_height = 600 + + canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white") + canvas.pack() + + renderer = GraphicsRenderer(canvas) + + # ------------------------------------------------------------ + # Square animation (non-fluent, defined in a single expression) + # ------------------------------------------------------------ + + square_animation = Animation( + steps=[ + Rotate(60), + Move(200, 0), + Scale(1.3), + Fade(200), + Move(0, 120), + Fade(40), + ], + durations=[ + 1.0, # rotate + 1.0, # move right + 1.0, # scale + 0.8, # fade to 200 + 1.0, # move downward + 0.8, # fade to 40 + ], + start_time=0.0, + ) + + square = Shape( + shape_id="square", + points=[(250, 200), (350, 200), (350, 300), (250, 300)], + color="#444444", + animation=square_animation, + ) + + # ------------------------------------------------------------ + # Triangle animation (non-fluent, also one expression) + # ------------------------------------------------------------ + + triangle_animation = Animation( + steps=[ + Fade(220), + Move(-80, 40), + Scale(0.9), + ], + durations=[ + 1.0, # fade + 1.0, # move up-left + 1.0, # scale down + ], + start_time=0.5, + ) + + triangle = Shape( + shape_id="triangle", + points=[(500, 250), (550, 350), (450, 350)], + color="#888888", + animation=triangle_animation, + ) + + shapes = [square, triangle] + + # ------------------------------------------------------------ + # Event binding + # ------------------------------------------------------------ + + def start_animation(event: Any): + play_scene(renderer, shapes) + + canvas.bind("", start_animation) + root.bind("", start_animation) + + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/2026/fluent/starting_point.py b/2026/fluent/starting_point.py deleted file mode 100644 index 4e7a4389..00000000 --- a/2026/fluent/starting_point.py +++ /dev/null @@ -1,40 +0,0 @@ - -import tkinter as tk - -from graphics import GraphicsRenderer, ShapeData, Color - - -def main() -> None: - root = tk.Tk() - root.title("ShapyMcShapeface Renderer") - - canvas_width = 800 - canvas_height = 600 - - canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white") - canvas.pack() - - renderer = GraphicsRenderer(canvas) - - # ------------------------------------------------------------ - # Define a few shapes - # ------------------------------------------------------------ - - square_points: ShapeData = [(250, 200), (350, 200), (350, 300), (250, 300)] - square_color: Color = "#444444" - - triangle_points: ShapeData = [(500, 250), (550, 350), (450, 350)] - triangle_color: Color = "#888888" - - # ------------------------------------------------------------ - # Render shapes using the renderer - # ------------------------------------------------------------ - - renderer.render("square", square_points, square_color) - renderer.render("triangle", triangle_points, triangle_color) - - root.mainloop() - - -if __name__ == "__main__": - main() \ No newline at end of file From c560005b7e61cb613cee5a429e270ffb000bb249 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 2 Dec 2025 16:31:56 +0100 Subject: [PATCH 074/113] Added Python features code examples. --- 2026/features/after_caching_and_sales.py | 97 ++++++++++++++ 2026/features/after_contextvars_and_match.py | 48 +++++++ 2026/features/after_exitstack.py | 24 ++++ 2026/features/before_caching_and_sales.py | 123 ++++++++++++++++++ 2026/features/before_contextvars_and_match.py | 39 ++++++ 2026/features/before_exitstack.py | 34 +++++ 2026/features/generate_sales_csv.py | 25 ++++ 2026/features/pyproject.toml | 6 + 2026/features/rates/broken.json | 1 + 2026/features/rates/eur.json | 1 + 2026/features/uv.lock | 8 ++ 11 files changed, 406 insertions(+) create mode 100644 2026/features/after_caching_and_sales.py create mode 100644 2026/features/after_contextvars_and_match.py create mode 100644 2026/features/after_exitstack.py create mode 100644 2026/features/before_caching_and_sales.py create mode 100644 2026/features/before_contextvars_and_match.py create mode 100644 2026/features/before_exitstack.py create mode 100644 2026/features/generate_sales_csv.py create mode 100644 2026/features/pyproject.toml create mode 100644 2026/features/rates/broken.json create mode 100644 2026/features/rates/eur.json create mode 100644 2026/features/uv.lock diff --git a/2026/features/after_caching_and_sales.py b/2026/features/after_caching_and_sales.py new file mode 100644 index 00000000..e932e122 --- /dev/null +++ b/2026/features/after_caching_and_sales.py @@ -0,0 +1,97 @@ +import json +from contextlib import suppress +from dataclasses import dataclass, replace +from functools import cache +from itertools import pairwise +from pathlib import Path +from typing import Any, Protocol, Sequence + + +@cache +def total_from_file(path: Path) -> float: + """Compute total sales by reading and parsing a large CSV file.""" + print(f"Reading file: {path.name}") + total = 0.0 + with path.open() as f: + for line in f: + _, amount = line.strip().split(",") + total += float(amount) + return total + + +class RateFetcher(Protocol): + def get_rate(self, currency: str) -> float: ... + + +class StaticRateFetcher: + """Simple fake fetcher for demonstration.""" + + def get_rate(self, currency: str) -> float: + rates = {"EUR": 1.1, "USD": 1.0} + return rates.get(currency, 1.0) + + +@dataclass(frozen=True) +class Sale: + amount: float + currency: str + converted: float | None = None + + +def convert_sale(sale: Sale, fetcher: RateFetcher) -> Sale: + rate = fetcher.get_rate(sale.currency) + return replace(sale, converted=sale.amount * rate) + + +def compute_sales_deltas(numbers: Sequence[float]) -> list[float]: + return [b - a for a, b in pairwise(numbers)] + + +def read_large_file(path: Path) -> int: + """Count bytes using streaming reads.""" + total = 0 + with path.open("rb") as f: + while chunk := f.read(4096): + total += len(chunk) + return total + + +def load_all_json(directory: Path) -> dict[str, Any]: + results: dict[str, Any] = {} + for path in directory.glob("*.json"): + with suppress(Exception): + results[path.stem] = json.loads(path.read_text()) + return results + + +def main() -> None: + print("\n--- Caching Demo (File Parsing) ---") + print(total_from_file(Path("sales_2025_Q1.csv"))) # reads the file + print(total_from_file(Path("sales_2025_Q1.csv"))) # instant + + print("\n--- Immutable Dataclass Demo ---") + sale = Sale(amount=100, currency="EUR") + new_sale = convert_sale(sale, StaticRateFetcher()) + print(new_sale) + + print("\n--- Pairwise Demo ---") + numbers = [120, 150, 200, 180] + print(compute_sales_deltas(numbers)) + + print("\n--- Assignment Expression Demo ---") + binary = Path("example.bin") + binary.write_bytes(b"x" * 10_000) + print(read_large_file(binary)) + + print("\n--- Pathlib + suppress Demo ---") + directory = Path("rates") + directory.mkdir(exist_ok=True) + (directory / "eur.json").write_text(json.dumps({"rate": 1.1})) + (directory / "broken.json").write_text("NOT JSON") + print(load_all_json(directory)) + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/2026/features/after_contextvars_and_match.py b/2026/features/after_contextvars_and_match.py new file mode 100644 index 00000000..031cdec8 --- /dev/null +++ b/2026/features/after_contextvars_and_match.py @@ -0,0 +1,48 @@ +# example_contextvars_and_match.py +import asyncio +import contextvars +import random + +request_id = contextvars.ContextVar("request_id", default="unknown") + + +def log(msg: str): + print(f"[Request {request_id.get()}] {msg}") + + +def classify_amount(amount: float) -> str: + match amount: + case x if x < 0: + return "invalid" + case 0: + return "zero" + case x if x > 10_000: + return "large" + case _: + return "normal" + + +async def process_user(user_id: int): + log(f"Processing user {user_id}") + amount = random.choice([-10, 0, 42, 50_000]) + log(f"Amount {amount} โ†’ {classify_amount(amount)}") + await asyncio.sleep(0.2) + + +async def handle_request(req_id: str, user_id: int): + token = request_id.set(req_id) + try: + await process_user(user_id) + finally: + request_id.reset(token) + + +async def main(): + await asyncio.gather( + handle_request("abc123", 1), + handle_request("xyz789", 2), + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2026/features/after_exitstack.py b/2026/features/after_exitstack.py new file mode 100644 index 00000000..fdd9f567 --- /dev/null +++ b/2026/features/after_exitstack.py @@ -0,0 +1,24 @@ +from contextlib import ExitStack +from pathlib import Path + + +def read_files(paths: list[Path]) -> list[str]: + with ExitStack() as stack: + files = [stack.enter_context(p.open()) for p in paths if p.exists()] + return [f.read() for f in files] + + +def main(): + p1 = Path("file1.txt") + p2 = Path("file2.txt") + p1.write_text("Hello!") + p2.write_text("World!") + + print(read_files([p1, p2])) + + p1.unlink() + p2.unlink() + + +if __name__ == "__main__": + main() diff --git a/2026/features/before_caching_and_sales.py b/2026/features/before_caching_and_sales.py new file mode 100644 index 00000000..33e14683 --- /dev/null +++ b/2026/features/before_caching_and_sales.py @@ -0,0 +1,123 @@ +import json +import os +from typing import Any, Sequence + + +# No dataclasses, no immutability +class Sale: + def __init__(self, amount: float, currency: str): + self.amount = amount + self.currency = currency + self.converted: float | None = None + + +# ------------------------------------------------------------------- +# Expensive CSV parsing (NO caching) +# ------------------------------------------------------------------- + + +def total_from_file(path: str) -> float: + """Compute total sales by reading and parsing a large CSV file.""" + print(f"Reading file: {path}") + total = 0.0 + with open(path) as f: + for line in f: + _, amount = line.strip().split(",") + total += float(amount) + return total + + +# ------------------------------------------------------------------- +# NO protocols โ€” duck typing +# ------------------------------------------------------------------- + + +class StaticRateFetcher: + def get_rate(self, currency: str) -> float: + rates = {"EUR": 1.1, "USD": 1.0} + return rates.get(currency, 1.0) + + +# Mutation instead of replace() +def convert_sale(sale: Sale, fetcher: StaticRateFetcher) -> Sale: + rate = fetcher.get_rate(sale.currency) + sale.converted = sale.amount * rate + return sale + + +# ------------------------------------------------------------------- +# Manual adjacent iteration (NO itertools.pairwise) +# ------------------------------------------------------------------- + + +def compute_sales_deltas(numbers: Sequence[float]) -> list[float]: + deltas: list[float] = [] + for i in range(len(numbers) - 1): + deltas.append(numbers[i + 1] - numbers[i]) + return deltas + + +# ------------------------------------------------------------------- +# Assignment expression BEFORE: manual double-read +# ------------------------------------------------------------------- + + +def read_large_file(path: str) -> int: + total = 0 + f = open(path, "rb") + chunk = f.read(4096) + while chunk: + total += len(chunk) + chunk = f.read(4096) + f.close() + return total + + +# ------------------------------------------------------------------- +# BEFORE: No suppress, no pathlib file iteration +# ------------------------------------------------------------------- + + +def load_all_json(directory: str) -> dict[str, Any]: + results: dict[str, Any] = {} + for filename in os.listdir(directory): + if not filename.endswith(".json"): + continue + try: + with open(os.path.join(directory, filename)) as f: + results[filename[:-5]] = json.load(f) + except Exception: + pass # fallback + return results + + +# ------------------------------------------------------------------- +# Demo +# ------------------------------------------------------------------- + + +def main() -> None: + print("\n--- Caching Demo (Before) ---") + print(total_from_file("sales_2025_Q1.csv")) + print(total_from_file("sales_2025_Q1.csv")) # slow again + + print("\n--- Mutable Sale Demo ---") + sale = Sale(100, "EUR") + sale = convert_sale(sale, StaticRateFetcher()) + print(sale.amount, sale.currency, sale.converted) + + print("\n--- Manual deltas ---") + numbers = [120, 150, 200, 180] + print(compute_sales_deltas(numbers)) + + print("\n--- Manual chunk loop ---") + with open("example.bin", "wb") as f: + f.write(b"x" * 10_000) + print(read_large_file("example.bin")) + + print("\n--- Manual JSON loading ---") + print(load_all_json("rates")) + + +if __name__ == "__main__": + main() diff --git a/2026/features/before_contextvars_and_match.py b/2026/features/before_contextvars_and_match.py new file mode 100644 index 00000000..4fcc9425 --- /dev/null +++ b/2026/features/before_contextvars_and_match.py @@ -0,0 +1,39 @@ +# before_contextvars_and_match.py +import asyncio +import random + + +# BEFORE: request_id passed manually +async def process_user(user_id: int, request_id: str): + print(f"[Request {request_id}] Processing user {user_id}") + amount = random.choice([-10, 0, 42, 50_000]) + print(f"[Request {request_id}] Amount {amount} โ†’ {classify_amount_before(amount)}") + await asyncio.sleep(0.2) + + +# BEFORE: nested if/elif +def classify_amount_before(amount: float) -> str: + if amount < 0: + return "invalid" + elif amount == 0: + return "zero" + elif amount > 10_000: + return "large" + else: + return "normal" + + +# BEFORE: all state manually threaded into calls +async def handle_request(request_id: str, user_id: int): + await process_user(user_id, request_id) + + +async def main(): + await asyncio.gather( + handle_request("abc123", 1), + handle_request("xyz789", 2), + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2026/features/before_exitstack.py b/2026/features/before_exitstack.py new file mode 100644 index 00000000..82342239 --- /dev/null +++ b/2026/features/before_exitstack.py @@ -0,0 +1,34 @@ +# before_exitstack.py +from pathlib import Path + + +# BEFORE: deep nesting & manual cleanup +def read_files(paths: list[Path]) -> list[str]: + results: list[str] = [] + for p in paths: + if not p.exists(): + continue + f = p.open() + try: + data = f.read() + results.append(data) + finally: + f.close() + return results + + +def main(): + p1 = Path("file1.txt") + p2 = Path("file2.txt") + + p1.write_text("Hello!") + p2.write_text("World!") + + print(read_files([p1, p2])) + + p1.unlink() + p2.unlink() + + +if __name__ == "__main__": + main() diff --git a/2026/features/generate_sales_csv.py b/2026/features/generate_sales_csv.py new file mode 100644 index 00000000..6b7389d8 --- /dev/null +++ b/2026/features/generate_sales_csv.py @@ -0,0 +1,25 @@ +import random +from pathlib import Path + + +def generate_csv(path: Path, rows: int = 10_000_000) -> None: + """ + Generate a CSV file with `rows` number of sales entries. + Format: sale_id,amount + Example: 1,12.50 + """ + with path.open("w") as f: + for i in range(1, rows + 1): + amount = round(random.uniform(1.0, 500.0), 2) + f.write(f"{i},{amount}\n") + + print(f"Generated {rows} rows โ†’ {path}") + + +def main() -> None: + path = Path("sales_2025_Q1.csv") + generate_csv(path) + + +if __name__ == "__main__": + main() diff --git a/2026/features/pyproject.toml b/2026/features/pyproject.toml new file mode 100644 index 00000000..935450b9 --- /dev/null +++ b/2026/features/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "python_features" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/features/rates/broken.json b/2026/features/rates/broken.json new file mode 100644 index 00000000..7d174914 --- /dev/null +++ b/2026/features/rates/broken.json @@ -0,0 +1 @@ +NOT JSON \ No newline at end of file diff --git a/2026/features/rates/eur.json b/2026/features/rates/eur.json new file mode 100644 index 00000000..440be3d4 --- /dev/null +++ b/2026/features/rates/eur.json @@ -0,0 +1 @@ +{"rate": 1.1} \ No newline at end of file diff --git a/2026/features/uv.lock b/2026/features/uv.lock new file mode 100644 index 00000000..33a10936 --- /dev/null +++ b/2026/features/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "python-features" +version = "0.1.0" +source = { virtual = "." } From b1bb4efec67da1f04df2bd3b58b33baf37dc003e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 9 Dec 2025 16:25:13 +0100 Subject: [PATCH 075/113] Fixed issue in contextvars example. --- 2026/features/after_contextvars_and_match.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2026/features/after_contextvars_and_match.py b/2026/features/after_contextvars_and_match.py index 031cdec8..2b8db042 100644 --- a/2026/features/after_contextvars_and_match.py +++ b/2026/features/after_contextvars_and_match.py @@ -23,9 +23,9 @@ def classify_amount(amount: float) -> str: async def process_user(user_id: int): - log(f"Processing user {user_id}") + print(f"[Request {request_id.get()}] Processing user {user_id}") amount = random.choice([-10, 0, 42, 50_000]) - log(f"Amount {amount} โ†’ {classify_amount(amount)}") + print(f"[Request {request_id.get()}] Amount {amount} โ†’ {classify_amount(amount)}") await asyncio.sleep(0.2) From 14baa39fda8f24f251423ffa305ab544a7030bd6 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 27 Nov 2025 11:16:36 +0100 Subject: [PATCH 076/113] Added code example. --- 2026/dataclass/main.py | 114 ++++++++++++++++++++++++++++++++++ 2026/dataclass/pyproject.toml | 6 ++ 2026/dataclass/uv.lock | 8 +++ 3 files changed, 128 insertions(+) create mode 100644 2026/dataclass/main.py create mode 100644 2026/dataclass/pyproject.toml create mode 100644 2026/dataclass/uv.lock diff --git a/2026/dataclass/main.py b/2026/dataclass/main.py new file mode 100644 index 00000000..7cd76092 --- /dev/null +++ b/2026/dataclass/main.py @@ -0,0 +1,114 @@ +from abc import ABC, abstractmethod +from dataclasses import asdict, astuple, dataclass, field + +# ================================================================ +# USER DATACLASS WITH ALL FEATURES FROM THE VIDEO +# ================================================================ + + +@dataclass(order=True, slots=True, kw_only=True, frozen=True) +class User: + name: str + email: str + tags: list[str] = field(default_factory=list[str]) + slug: str = field(init=False) + + def __post_init__(self): + # Normalize name and create slug + normalized_name = self.name.strip().title() + slugified = normalized_name.lower().replace(" ", "-") + + object.__setattr__(self, "name", normalized_name) + object.__setattr__(self, "slug", slugified) + + @property + def domain(self) -> str: + """Return the domain part of the email address.""" + return self.email.split("@")[-1] + + def contact_card(self) -> str: + """Return a formatted contact card.""" + return f"{self.name} <{self.email}>" + + @classmethod + def from_email(cls, email: str) -> "User": + """Create a User from only an email address.""" + local = email.split("@")[0].replace(".", " ") + name = local.title() + return cls(name=name, email=email) + + +# ================================================================ +# ABSTRACT DATACLASS EXAMPLE +# ================================================================ + + +@dataclass +class Account(ABC): + owner: str + base_fee: float + + @property + @abstractmethod + def monthly_fee(self) -> float: ... + + +@dataclass +class FreeAccount(Account): + @property + def monthly_fee(self) -> float: + return 0.0 + + +@dataclass +class PremiumAccount(Account): + extra_storage_gb: int = 100 + + @property + def monthly_fee(self) -> float: + return self.base_fee + (self.extra_storage_gb * 0.10) + + +# ================================================================ +# MAIN WITH RUNNING EXAMPLES +# ================================================================ + + +def main(): + print("\n=== Creating Users ===") + u1 = User(name="alice", email="alice@example.com") + u2 = User(name="bob", email="bob@example.com") + print("u1:", u1) + print("u2:", u2) + + print("\n=== Using from_email constructor ===") + u3 = User.from_email("john.doe@company.com") + print("u3:", u3) + + print("\n=== Comparing Users (order=True) ===") + print("u1 < u2:", u1 < u2) + print("Sorted:", sorted([u2, u1, u3])) + + print("\n=== Frozen Dataclass Behavior ===") + try: + u1.name = "Charlie" # should fail + except Exception as e: + print("Attempting to reassign u1.name:", e) + + print("\n=== Shallow Immutability Example ===") + u1.tags.append("admin") + print("u1.tags after append:", u1.tags) + + print("\n=== Serialization ===") + print("asdict(u1):", asdict(u1)) + print("astuple(u1):", astuple(u1)) + + print("\n=== Account Types (Abstract Dataclasses) ===") + free = FreeAccount(owner="Alice", base_fee=0) + premium = PremiumAccount(owner="Bob", base_fee=5) + print("FreeAccount monthly fee:", free.monthly_fee) + print("PremiumAccount monthly fee:", premium.monthly_fee) + + +if __name__ == "__main__": + main() diff --git a/2026/dataclass/pyproject.toml b/2026/dataclass/pyproject.toml new file mode 100644 index 00000000..238121d5 --- /dev/null +++ b/2026/dataclass/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "dataclass" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/dataclass/uv.lock b/2026/dataclass/uv.lock new file mode 100644 index 00000000..6e031868 --- /dev/null +++ b/2026/dataclass/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "dataclass" +version = "0.1.0" +source = { virtual = "." } From 62477c1d4528a615f3ff2f6a2ffe1eb8b2c41c0d Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Mon, 12 Jan 2026 09:03:01 +0100 Subject: [PATCH 077/113] Worked on cqrs example. --- 2026/cqrs/EXAMPLES.md | 15 ++ 2026/cqrs/after.py | 350 +++++++++++++++++++++++++++++++++ 2026/cqrs/before.py | 224 +++++++++++++++++++++ 2026/cqrs/db.py | 18 ++ 2026/cqrs/docker-compose.yml | 14 ++ 2026/cqrs/pyproject.toml | 9 + 2026/cqrs/uv.lock | 368 +++++++++++++++++++++++++++++++++++ 7 files changed, 998 insertions(+) create mode 100644 2026/cqrs/EXAMPLES.md create mode 100644 2026/cqrs/after.py create mode 100644 2026/cqrs/before.py create mode 100644 2026/cqrs/db.py create mode 100644 2026/cqrs/docker-compose.yml create mode 100644 2026/cqrs/pyproject.toml create mode 100644 2026/cqrs/uv.lock diff --git a/2026/cqrs/EXAMPLES.md b/2026/cqrs/EXAMPLES.md new file mode 100644 index 00000000..8567eae6 --- /dev/null +++ b/2026/cqrs/EXAMPLES.md @@ -0,0 +1,15 @@ +In order to run a MongoDB instance locally, make sure you have docker installed on your machine and run: + +``` +docker compose up -d +``` + +Then run the FastAPI app as follows: + +``` +uv sync +source .venv/bin/activate +uvicorn before:app --reload # to run the before version of the code +``` + +Here are a few example requests you can use to test the FastAPI app: \ No newline at end of file diff --git a/2026/cqrs/after.py b/2026/cqrs/after.py new file mode 100644 index 00000000..8dfb69ed --- /dev/null +++ b/2026/cqrs/after.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any, Literal + +from bson import ObjectId +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from pymongo import AsyncMongoClient +from pymongo.asynchronous.database import AsyncDatabase + +# ---------------------------- +# Types / helpers +# ---------------------------- + +Status = Literal["open", "triaged", "closed"] + +PREVIEW_LEN = 80 + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +def oid_str(oid: ObjectId) -> str: + return str(oid) + + +def parse_object_id(ticket_id: str) -> ObjectId: + try: + return ObjectId(ticket_id) + except Exception as e: + raise HTTPException(status_code=400, detail="Invalid ticket id") from e + + +def make_preview(message: str) -> str: + msg = message.strip().replace("\n", " ") + return msg if len(msg) <= PREVIEW_LEN else msg[: PREVIEW_LEN - 1] + "โ€ฆ" + + +# ---------------------------- +# MongoDB configuration (local + auth) +# ---------------------------- + +MONGODB_URI = "mongodb://root:example@localhost:27017/?authSource=admin" +DATABASE_NAME = "cqrs_demo" + +COMMANDS_COLL = "ticket_commands" # source of truth +READS_COLL = "ticket_reads" # read projection for list/dashboard + +# ---------------------------- +# FastAPI app + DB lifecycle +# ---------------------------- + +app = FastAPI() + +mongo_client: AsyncMongoClient | None = None +db: AsyncDatabase | None = None + + +@app.on_event("startup") +async def startup() -> None: + global mongo_client, db + + mongo_client = AsyncMongoClient(MONGODB_URI) + db = mongo_client[DATABASE_NAME] + + # Write-side indexes (source of truth) + await db[COMMANDS_COLL].create_index("status") + await db[COMMANDS_COLL].create_index("updated_at") + + # Read-side indexes (optimized for query patterns) + await db[READS_COLL].create_index("status") + await db[READS_COLL].create_index("updated_at") + await db[READS_COLL].create_index("has_note") # cheap example of a read-only filter + + +@app.on_event("shutdown") +async def shutdown() -> None: + if mongo_client is not None: + mongo_client.close() + + +def get_db() -> AsyncDatabase: + assert db is not None + return db + + +# ---------------------------- +# Commands (write DTOs) +# ---------------------------- + + +class CreateTicket(BaseModel): + customer_id: str + subject: str + message: str + + +class UpdateStatus(BaseModel): + new_status: Status + + +class AddAgentNote(BaseModel): + note: str + + +# ---------------------------- +# Queries (read DTOs) +# ---------------------------- + + +class TicketListItem(BaseModel): + id: str + subject: str + status: Status + updated_at: datetime + preview: str + has_note: bool + + +class TicketDetails(BaseModel): + id: str + customer_id: str + subject: str + message: str + status: Status + agent_note: str | None = None + created_at: datetime + updated_at: datetime + + +# ---------------------------- +# Command handlers (write side) +# ---------------------------- + + +async def cmd_create_ticket(db: AsyncDatabase, cmd: CreateTicket) -> str: + now = utcnow() + doc: dict[str, Any] = { + "customer_id": cmd.customer_id, + "subject": cmd.subject, + "message": cmd.message, + "status": "open", + "agent_note": None, + "created_at": now, + "updated_at": now, + } + res = await db[COMMANDS_COLL].insert_one(doc) + return oid_str(res.inserted_id) + + +async def cmd_update_status( + db: AsyncDatabase, ticket_id: str, cmd: UpdateStatus +) -> None: + _id = parse_object_id(ticket_id) + + existing = await db[COMMANDS_COLL].find_one({"_id": _id}) + if existing is None: + raise ValueError("Ticket not found") + + if existing["status"] == "closed" and cmd.new_status != "closed": + raise ValueError("Closed tickets cannot be reopened") + + await db[COMMANDS_COLL].update_one( + {"_id": _id}, + {"$set": {"status": cmd.new_status, "updated_at": utcnow()}}, + ) + + +async def cmd_add_agent_note( + db: AsyncDatabase, ticket_id: str, cmd: AddAgentNote +) -> None: + _id = parse_object_id(ticket_id) + + existing = await db[COMMANDS_COLL].find_one({"_id": _id}) + if existing is None: + raise ValueError("Ticket not found") + + await db[COMMANDS_COLL].update_one( + {"_id": _id}, + {"$set": {"agent_note": cmd.note, "updated_at": utcnow()}}, + ) + + +# ---------------------------- +# Projector (build read model) +# ---------------------------- + + +async def project_ticket(db: AsyncDatabase, ticket_id: str) -> None: + """ + Read model goal: + - store exactly what the list/dashboard needs + - in a shape that is cheap to query + - derived fields (preview, has_note) are computed once here + """ + _id = parse_object_id(ticket_id) + doc = await db[COMMANDS_COLL].find_one({"_id": _id}) + if doc is None: + return + + note = (doc.get("agent_note") or "").strip() + + read_doc: dict[str, Any] = { + "_id": doc["_id"], # same id on read side + "subject": doc["subject"], + "status": doc["status"], + "updated_at": doc["updated_at"], + "preview": make_preview(doc.get("message", "")), + "has_note": bool(note), + } + + await db[READS_COLL].update_one( + {"_id": doc["_id"]}, + {"$set": read_doc}, + upsert=True, + ) + + +# ---------------------------- +# Command endpoints (write API) +# ---------------------------- + + +@app.post("/tickets") +async def create_ticket(cmd: CreateTicket) -> dict[str, str]: + db = get_db() + ticket_id = await cmd_create_ticket(db, cmd) + await project_ticket(db, ticket_id) # synchronous for demo + return {"id": ticket_id} + + +@app.post("/tickets/{ticket_id}/status") +async def update_status(ticket_id: str, cmd: UpdateStatus) -> dict[str, str]: + db = get_db() + try: + await cmd_update_status(db, ticket_id, cmd) + await project_ticket(db, ticket_id) + return {"status": "ok"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@app.post("/tickets/{ticket_id}/agent-note") +async def add_agent_note(ticket_id: str, cmd: AddAgentNote) -> dict[str, str]: + db = get_db() + try: + await cmd_add_agent_note(db, ticket_id, cmd) + await project_ticket(db, ticket_id) + return {"status": "ok"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +# ---------------------------- +# Query endpoints (read API) +# ---------------------------- + + +@app.get("/tickets", response_model=list[TicketListItem]) +async def list_tickets( + status: Status | None = None, + has_note: bool | None = None, + limit: int = 20, + skip: int = 0, +) -> list[TicketListItem]: + """ + Now reads are truly read-optimized: + - no need to pull message/agent_note + - preview + has_note already exist + - can index/filter on read-only fields + """ + db = get_db() + + query: dict[str, Any] = {} + if status is not None: + query["status"] = status + if has_note is not None: + query["has_note"] = has_note + + cursor = ( + db[READS_COLL] + .find( + query, + projection={ + "subject": 1, + "status": 1, + "updated_at": 1, + "preview": 1, + "has_note": 1, + }, + ) + .sort("updated_at", -1) + .skip(skip) + .limit(limit) + ) + + out: list[TicketListItem] = [] + async for doc in cursor: + out.append( + TicketListItem( + id=oid_str(doc["_id"]), + subject=doc["subject"], + status=doc["status"], + updated_at=doc["updated_at"], + preview=doc["preview"], + has_note=doc["has_note"], + ) + ) + return out + + +@app.get("/tickets/{ticket_id}", response_model=TicketDetails) +async def get_ticket(ticket_id: str) -> TicketDetails: + """ + For details we read from the source of truth. + (You *could* also build a separate details projection if needed.) + """ + db = get_db() + _id = parse_object_id(ticket_id) + + doc = await db[COMMANDS_COLL].find_one({"_id": _id}) + if doc is None: + raise HTTPException(status_code=404, detail="Ticket not found") + + return TicketDetails( + id=ticket_id, + customer_id=doc["customer_id"], + subject=doc["subject"], + message=doc["message"], + status=doc["status"], + agent_note=doc.get("agent_note"), + created_at=doc["created_at"], + updated_at=doc["updated_at"], + ) + + +@app.get("/dashboard") +async def dashboard() -> dict[str, int]: + """ + Dashboard runs on the read model, not the write model. + """ + db = get_db() + pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}] + + counts: dict[str, int] = {"open": 0, "triaged": 0, "closed": 0} + async for row in db[READS_COLL].aggregate(pipeline): + counts[str(row["_id"])] = int(row["count"]) + return counts diff --git a/2026/cqrs/before.py b/2026/cqrs/before.py new file mode 100644 index 00000000..d1f52899 --- /dev/null +++ b/2026/cqrs/before.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any, Literal + +from bson import ObjectId +from db import get_db, shutdown_db +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +Status = Literal["open", "triaged", "closed"] + +PREVIEW_LEN = 80 + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +def oid_str(oid: ObjectId) -> str: + return str(oid) + + +def parse_object_id(ticket_id: str) -> ObjectId: + try: + return ObjectId(ticket_id) + except Exception as e: + raise HTTPException(status_code=400, detail="Invalid ticket id") from e + + +def make_preview(message: str) -> str: + msg = message.strip().replace("\n", " ") + return msg if len(msg) <= PREVIEW_LEN else msg[: PREVIEW_LEN - 1] + "โ€ฆ" + + +app = FastAPI() + + +@app.on_event("startup") +async def startup() -> None: + db = get_db() + + # Practical indexes for common queries (still one collection) + await db[TICKETS_COLL].create_index("status") + await db[TICKETS_COLL].create_index("updated_at") + + +@app.on_event("shutdown") +async def shutdown() -> None: + shutdown_db() + + +# ---- Pydantic models ---- + + +class TicketIn(BaseModel): + customer_id: str + subject: str + message: str + + +class TicketPatch(BaseModel): + status: Status | None = None + agent_note: str | None = None + + +class TicketDetails(BaseModel): + id: str + customer_id: str + subject: str + message: str + status: Status + agent_note: str | None = None + created_at: datetime + updated_at: datetime + + +class TicketListItem(BaseModel): + id: str + subject: str + status: Status + updated_at: datetime + preview: str + has_note: bool + + +# ---- Endpoints (no CQRS yet) ---- + + +@app.post("/tickets", response_model=TicketDetails) +async def create_ticket(payload: TicketIn) -> TicketDetails: + db = get_db() + now = utcnow() + + doc: dict[str, Any] = { + "customer_id": payload.customer_id, + "subject": payload.subject, + "message": payload.message, + "status": "open", + "agent_note": None, + "created_at": now, + "updated_at": now, + } + + res = await db[TICKETS_COLL].insert_one(doc) + return TicketDetails(id=oid_str(res.inserted_id), **doc) + + +@app.patch("/tickets/{ticket_id}", response_model=TicketDetails) +async def update_ticket(ticket_id: str, patch: TicketPatch) -> TicketDetails: + db = get_db() + + existing = await db[TICKETS_COLL].find_one({"_id": ticket_id}) + if existing is None: + raise HTTPException(status_code=404, detail="Ticket not found") + + # Business rule mixed with patching and persistence + if patch.status is not None: + if existing["status"] == "closed" and patch.status != "closed": + raise HTTPException( + status_code=400, detail="Closed tickets cannot be reopened" + ) + + update: dict[str, Any] = {"updated_at": utcnow()} + if patch.status is not None: + update["status"] = patch.status + if patch.agent_note is not None: + update["agent_note"] = patch.agent_note + + await db[TICKETS_COLL].update_one({"_id": _id}, {"$set": update}) + + updated = {**existing, **update} + return TicketDetails( + id=ticket_id, + customer_id=updated["customer_id"], + subject=updated["subject"], + message=updated["message"], + status=updated["status"], + agent_note=updated.get("agent_note"), + created_at=updated["created_at"], + updated_at=updated["updated_at"], + ) + + +@app.get("/tickets", response_model=list[TicketListItem]) +async def list_tickets( + status: Status | None = None, limit: int = 20, skip: int = 0 +) -> list[TicketListItem]: + """ + Pain point in the BEFORE version: + - The list view wants preview + has_note (read concerns). + - We compute them on every request. + - If this gets more complex (scores, denormalized fields, analytics), + this endpoint becomes a hotspot. + """ + db = get_db() + + query: dict[str, Any] = {} + if status is not None: + query["status"] = status + + # We still need message + agent_note to compute preview/has_note. + cursor = ( + db[TICKETS_COLL] + .find( + query, + projection={ + "subject": 1, + "status": 1, + "updated_at": 1, + "message": 1, + "agent_note": 1, + }, + ) + .sort("updated_at", -1) + .skip(skip) + .limit(limit) + ) + + out: list[TicketListItem] = [] + async for doc in cursor: + note = (doc.get("agent_note") or "").strip() + out.append( + TicketListItem( + id=oid_str(doc["_id"]), + subject=doc["subject"], + status=doc["status"], + updated_at=doc["updated_at"], + preview=make_preview(doc.get("message", "")), + has_note=bool(note), + ) + ) + return out + + +@app.get("/tickets/{ticket_id}", response_model=TicketDetails) +async def get_ticket(ticket_id: str) -> TicketDetails: + db = get_db() + + doc = await db[TICKETS_COLL].find_one({"_id": ticket_id}) + if doc is None: + raise HTTPException(status_code=404, detail="Ticket not found") + + return TicketDetails( + id=ticket_id, + customer_id=doc["customer_id"], + subject=doc["subject"], + message=doc["message"], + status=doc["status"], + agent_note=doc.get("agent_note"), + created_at=doc["created_at"], + updated_at=doc["updated_at"], + ) + + +@app.get("/dashboard") +async def dashboard() -> dict[str, int]: + db = get_db() + pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}] + + counts: dict[str, int] = {"open": 0, "triaged": 0, "closed": 0} + async for row in db[TICKETS_COLL].aggregate(pipeline): + counts[str(row["_id"])] = int(row["count"]) + return counts diff --git a/2026/cqrs/db.py b/2026/cqrs/db.py new file mode 100644 index 00000000..f68552fe --- /dev/null +++ b/2026/cqrs/db.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pymongo.asynchronous.database import AsyncDatabase + +MONGODB_URI = "mongodb://root:example@localhost:27017/?authSource=admin" +DATABASE_NAME = "cqrs_demo" +TICKETS_COLL = "tickets" + +mongo_client = AsyncMongoClient(MONGODB_URI) +db: AsyncDatabase | None = None + + +def get_db() -> AsyncDatabase: + return mongo_client[DATABASE_NAME] + + +def shutdown_db() -> None: + mongo_client.close() diff --git a/2026/cqrs/docker-compose.yml b/2026/cqrs/docker-compose.yml new file mode 100644 index 00000000..63063b05 --- /dev/null +++ b/2026/cqrs/docker-compose.yml @@ -0,0 +1,14 @@ +services: + mongodb: + image: mongo:7 + container_name: cqrs-mongodb + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: \ No newline at end of file diff --git a/2026/cqrs/pyproject.toml b/2026/cqrs/pyproject.toml new file mode 100644 index 00000000..8dd4d74a --- /dev/null +++ b/2026/cqrs/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "cqrs" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "fastapi>=0.128.0", + "pymongo>=4.16.0", + "uvicorn[standard]>=0.40.0", +] diff --git a/2026/cqrs/uv.lock b/2026/cqrs/uv.lock new file mode 100644 index 00000000..8d8b4efa --- /dev/null +++ b/2026/cqrs/uv.lock @@ -0,0 +1,368 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cqrs" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pymongo" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "pymongo", specifier = ">=4.16.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.40.0" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pymongo" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323, upload-time = "2026-01-07T18:05:48.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/9e/4d343f8d0512002fce17915a89477b9f916bda1205729e042d8f23acf194/pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66", size = 1026634, upload-time = "2026-01-07T18:04:54.359Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e3/341f88c5535df40c0450fda915f582757bb7d988cdfc92990a5e27c4c324/pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64", size = 1026252, upload-time = "2026-01-07T18:04:56.642Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/9471b22eb98f0a2ca0b8e09393de048502111b2b5b14ab1bd9e39708aab5/pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc", size = 2207399, upload-time = "2026-01-07T18:04:58.255Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/47c4d50b25a02f21764f140295a2efaa583ee7f17992a5e5fa542b3a690f/pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371", size = 2260595, upload-time = "2026-01-07T18:04:59.788Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1b/0ce1ce9dd036417646b2fe6f63b58127acff3cf96eeb630c34ec9cd675ff/pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b", size = 2366958, upload-time = "2026-01-07T18:05:01.942Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3c/a5a17c0d413aa9d6c17bc35c2b472e9e79cda8068ba8e93433b5f43028e9/pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3", size = 2346081, upload-time = "2026-01-07T18:05:03.576Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/f815533d1a88fb8a3b6c6e895bb085ffdae68ccb1e6ed7102202a307f8e2/pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6", size = 2246053, upload-time = "2026-01-07T18:05:05.459Z" }, + { url = "https://files.pythonhosted.org/packages/c6/88/4be3ec78828dc64b212c123114bd6ae8db5b7676085a7b43cc75d0131bd2/pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8", size = 989461, upload-time = "2026-01-07T18:05:07.018Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/ab8d5af76421b34db483c9c8ebc3a2199fb80ae63dc7e18f4cf1df46306a/pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35", size = 1017803, upload-time = "2026-01-07T18:05:08.499Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/98d68020728ac6423cf02d17cfd8226bf6cce5690b163d30d3f705e8297e/pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033", size = 997184, upload-time = "2026-01-07T18:05:09.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/00/dc3a271daf06401825b9c1f4f76f018182c7738281ea54b9762aea0560c1/pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe", size = 1083303, upload-time = "2026-01-07T18:05:11.702Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/b5375ee21d12eababe46215011ebc63801c0d2c5ffdf203849d0d79f9852/pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4", size = 1083233, upload-time = "2026-01-07T18:05:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e3/52efa3ca900622c7dcb56c5e70f15c906816d98905c22d2ee1f84d9a7b60/pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53", size = 2527438, upload-time = "2026-01-07T18:05:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/96/43b1be151c734e7766c725444bcbfa1de6b60cc66bfb406203746839dd25/pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc", size = 2600399, upload-time = "2026-01-07T18:05:16.794Z" }, + { url = "https://files.pythonhosted.org/packages/e7/62/fa64a5045dfe3a1cd9217232c848256e7bc0136cffb7da4735c5e0d30e40/pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f", size = 2720960, upload-time = "2026-01-07T18:05:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/7b/01577eb97e605502821273a5bc16ce0fb0be5c978fe03acdbff471471202/pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111", size = 2699344, upload-time = "2026-01-07T18:05:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/68/6ef6372d516f703479c3b6cbbc45a5afd307173b1cbaccd724e23919bb1a/pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098", size = 2577133, upload-time = "2026-01-07T18:05:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/b5337093bb01da852f945802328665f85f8109dbe91d81ea2afe5ff059b9/pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487", size = 1040560, upload-time = "2026-01-07T18:05:23.888Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/5b448cd1b103f3889d5713dda37304c81020ff88e38a826e8a75ddff4610/pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a", size = 1075081, upload-time = "2026-01-07T18:05:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/ddc794cdc8500f6f28c119c624252fb6dfb19481c6d7ed150f13cf468a6d/pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96", size = 1047725, upload-time = "2026-01-07T18:05:28.47Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] From e61f7470d8286dbf821439ee747295161593b0c0 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 22 Jan 2026 16:11:21 +0100 Subject: [PATCH 078/113] Updated code example --- 2026/cqrs/EXAMPLES.md | 36 ++++++++++++++++++++++++++++++- 2026/cqrs/after.py | 49 +++++++++---------------------------------- 2026/cqrs/before.py | 15 ++----------- 2026/cqrs/db.py | 24 ++++++++++++++------- 4 files changed, 64 insertions(+), 60 deletions(-) diff --git a/2026/cqrs/EXAMPLES.md b/2026/cqrs/EXAMPLES.md index 8567eae6..467f519a 100644 --- a/2026/cqrs/EXAMPLES.md +++ b/2026/cqrs/EXAMPLES.md @@ -12,4 +12,38 @@ source .venv/bin/activate uvicorn before:app --reload # to run the before version of the code ``` -Here are a few example requests you can use to test the FastAPI app: \ No newline at end of file +Here are a few example requests you can use to test the FastAPI app: + +# Create a ticket + +curl -X POST http://localhost:8000/tickets \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "cust-123", + "subject": "Login not working", + "message": "I cannot log in after resetting my password." + }' + +# Update ticket status + +curl -X PATCH http://localhost:8000/tickets/69722457531892ec7a576522 \ + -H "Content-Type: application/json" \ + -d '{ + "status": "triaged" + }' + +# Update ticket status (after version) + +curl -X POST http://localhost:8000/tickets/697239ff4e89771fad8ed94f/status \ + -H "Content-Type: application/json" \ + -d '{ + "new_status": "triaged" + }' + +# Add a note + +curl -X POST http://localhost:8000/tickets/697239ff4e89771fad8ed94f/agent-note \ + -H "Content-Type: application/json" \ + -d '{ + "note": "Customer reset their password twice; investigating auth service." + }' diff --git a/2026/cqrs/after.py b/2026/cqrs/after.py index 8dfb69ed..e8edb382 100644 --- a/2026/cqrs/after.py +++ b/2026/cqrs/after.py @@ -3,11 +3,9 @@ from datetime import datetime, timezone from typing import Any, Literal -from bson import ObjectId +from db import Database, get_db, oid_str, parse_object_id, shutdown_db from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from pymongo import AsyncMongoClient -from pymongo.asynchronous.database import AsyncDatabase # ---------------------------- # Types / helpers @@ -22,17 +20,6 @@ def utcnow() -> datetime: return datetime.now(timezone.utc) -def oid_str(oid: ObjectId) -> str: - return str(oid) - - -def parse_object_id(ticket_id: str) -> ObjectId: - try: - return ObjectId(ticket_id) - except Exception as e: - raise HTTPException(status_code=400, detail="Invalid ticket id") from e - - def make_preview(message: str) -> str: msg = message.strip().replace("\n", " ") return msg if len(msg) <= PREVIEW_LEN else msg[: PREVIEW_LEN - 1] + "โ€ฆ" @@ -42,8 +29,6 @@ def make_preview(message: str) -> str: # MongoDB configuration (local + auth) # ---------------------------- -MONGODB_URI = "mongodb://root:example@localhost:27017/?authSource=admin" -DATABASE_NAME = "cqrs_demo" COMMANDS_COLL = "ticket_commands" # source of truth READS_COLL = "ticket_reads" # read projection for list/dashboard @@ -54,16 +39,10 @@ def make_preview(message: str) -> str: app = FastAPI() -mongo_client: AsyncMongoClient | None = None -db: AsyncDatabase | None = None - @app.on_event("startup") async def startup() -> None: - global mongo_client, db - - mongo_client = AsyncMongoClient(MONGODB_URI) - db = mongo_client[DATABASE_NAME] + db = get_db() # Write-side indexes (source of truth) await db[COMMANDS_COLL].create_index("status") @@ -77,13 +56,7 @@ async def startup() -> None: @app.on_event("shutdown") async def shutdown() -> None: - if mongo_client is not None: - mongo_client.close() - - -def get_db() -> AsyncDatabase: - assert db is not None - return db + await shutdown_db() # ---------------------------- @@ -135,7 +108,7 @@ class TicketDetails(BaseModel): # ---------------------------- -async def cmd_create_ticket(db: AsyncDatabase, cmd: CreateTicket) -> str: +async def cmd_create_ticket(db: Database, cmd: CreateTicket) -> str: now = utcnow() doc: dict[str, Any] = { "customer_id": cmd.customer_id, @@ -150,9 +123,7 @@ async def cmd_create_ticket(db: AsyncDatabase, cmd: CreateTicket) -> str: return oid_str(res.inserted_id) -async def cmd_update_status( - db: AsyncDatabase, ticket_id: str, cmd: UpdateStatus -) -> None: +async def cmd_update_status(db: Database, ticket_id: str, cmd: UpdateStatus) -> None: _id = parse_object_id(ticket_id) existing = await db[COMMANDS_COLL].find_one({"_id": _id}) @@ -168,9 +139,7 @@ async def cmd_update_status( ) -async def cmd_add_agent_note( - db: AsyncDatabase, ticket_id: str, cmd: AddAgentNote -) -> None: +async def cmd_add_agent_note(db: Database, ticket_id: str, cmd: AddAgentNote) -> None: _id = parse_object_id(ticket_id) existing = await db[COMMANDS_COLL].find_one({"_id": _id}) @@ -188,7 +157,7 @@ async def cmd_add_agent_note( # ---------------------------- -async def project_ticket(db: AsyncDatabase, ticket_id: str) -> None: +async def project_ticket(db: Database, ticket_id: str) -> None: """ Read model goal: - store exactly what the list/dashboard needs @@ -345,6 +314,8 @@ async def dashboard() -> dict[str, int]: pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}] counts: dict[str, int] = {"open": 0, "triaged": 0, "closed": 0} - async for row in db[READS_COLL].aggregate(pipeline): + + cursor = await db[READS_COLL].aggregate(pipeline) + async for row in cursor: counts[str(row["_id"])] = int(row["count"]) return counts diff --git a/2026/cqrs/before.py b/2026/cqrs/before.py index d1f52899..9f1d3ab1 100644 --- a/2026/cqrs/before.py +++ b/2026/cqrs/before.py @@ -3,31 +3,20 @@ from datetime import datetime, timezone from typing import Any, Literal -from bson import ObjectId -from db import get_db, shutdown_db +from db import get_db, oid_str, shutdown_db from fastapi import FastAPI, HTTPException from pydantic import BaseModel Status = Literal["open", "triaged", "closed"] PREVIEW_LEN = 80 +TICKETS_COLL = "tickets" def utcnow() -> datetime: return datetime.now(timezone.utc) -def oid_str(oid: ObjectId) -> str: - return str(oid) - - -def parse_object_id(ticket_id: str) -> ObjectId: - try: - return ObjectId(ticket_id) - except Exception as e: - raise HTTPException(status_code=400, detail="Invalid ticket id") from e - - def make_preview(message: str) -> str: msg = message.strip().replace("\n", " ") return msg if len(msg) <= PREVIEW_LEN else msg[: PREVIEW_LEN - 1] + "โ€ฆ" diff --git a/2026/cqrs/db.py b/2026/cqrs/db.py index f68552fe..42c8eeb3 100644 --- a/2026/cqrs/db.py +++ b/2026/cqrs/db.py @@ -1,18 +1,28 @@ -from __future__ import annotations +from typing import Any, Mapping +from bson import ObjectId from pymongo.asynchronous.database import AsyncDatabase +from pymongo.asynchronous.mongo_client import AsyncMongoClient MONGODB_URI = "mongodb://root:example@localhost:27017/?authSource=admin" DATABASE_NAME = "cqrs_demo" -TICKETS_COLL = "tickets" -mongo_client = AsyncMongoClient(MONGODB_URI) -db: AsyncDatabase | None = None +type Database = AsyncDatabase[Mapping[str, Any]] +mongo_client = AsyncMongoClient[Mapping[str, Any]](MONGODB_URI) +db: Database | None = None -def get_db() -> AsyncDatabase: +def get_db() -> Database: return mongo_client[DATABASE_NAME] -def shutdown_db() -> None: - mongo_client.close() +async def shutdown_db() -> None: + await mongo_client.close() + + +def oid_str(oid: ObjectId) -> str: + return str(oid) + + +def parse_object_id(ticket_id: str) -> ObjectId: + return ObjectId(ticket_id) From 204f40040988963c4cad312856cd85d0ec3b1630 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 20 Jan 2026 16:55:57 +0100 Subject: [PATCH 079/113] Added ports example. --- 2026/ports/.gitignore | 1 + 2026/ports/after/adapters/__init__.py | 0 2026/ports/after/adapters/api.py | 54 ++++ .../after/adapters/sqlalchemy_inventory.py | 36 +++ 2026/ports/after/db.py | 25 ++ 2026/ports/after/domain/__init__.py | 0 2026/ports/after/domain/errors.py | 22 ++ 2026/ports/after/domain/models.py | 18 ++ 2026/ports/after/domain/ports.py | 8 + 2026/ports/after/domain/use_cases.py | 16 ++ 2026/ports/after/main.py | 20 ++ 2026/ports/after/wiring.py | 26 ++ 2026/ports/before_app.py | 94 +++++++ 2026/ports/pyproject.toml | 9 + 2026/ports/uv.lock | 246 ++++++++++++++++++ 15 files changed, 575 insertions(+) create mode 100644 2026/ports/.gitignore create mode 100644 2026/ports/after/adapters/__init__.py create mode 100644 2026/ports/after/adapters/api.py create mode 100644 2026/ports/after/adapters/sqlalchemy_inventory.py create mode 100644 2026/ports/after/db.py create mode 100644 2026/ports/after/domain/__init__.py create mode 100644 2026/ports/after/domain/errors.py create mode 100644 2026/ports/after/domain/models.py create mode 100644 2026/ports/after/domain/ports.py create mode 100644 2026/ports/after/domain/use_cases.py create mode 100644 2026/ports/after/main.py create mode 100644 2026/ports/after/wiring.py create mode 100644 2026/ports/before_app.py create mode 100644 2026/ports/pyproject.toml create mode 100644 2026/ports/uv.lock diff --git a/2026/ports/.gitignore b/2026/ports/.gitignore new file mode 100644 index 00000000..751f18d8 --- /dev/null +++ b/2026/ports/.gitignore @@ -0,0 +1 @@ +**/*.db \ No newline at end of file diff --git a/2026/ports/after/adapters/__init__.py b/2026/ports/after/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2026/ports/after/adapters/api.py b/2026/ports/after/adapters/api.py new file mode 100644 index 00000000..122e9315 --- /dev/null +++ b/2026/ports/after/adapters/api.py @@ -0,0 +1,54 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field + +from ..domain.errors import InvalidQuantity, OutOfStock, UnknownSku +from ..domain.models import OrderRequest, Sku, UserId +from ..domain.ports import InventoryPort +from ..domain.use_cases import place_order + +router = APIRouter() + + +class PlaceOrderIn(BaseModel): + user_id: int + sku: str + qty: int = Field(..., gt=0) + + +class PlaceOrderOut(BaseModel): + sku: str + qty: int + remaining_stock: int + + +def get_inventory() -> InventoryPort: + """ + Stub for FastAPI DI. The real provider is wired in main.py. + """ + raise NotImplementedError + + +@router.post("/orders", response_model=PlaceOrderOut) +def place_order_endpoint( + payload: PlaceOrderIn, + inventory: InventoryPort = Depends(get_inventory), +) -> PlaceOrderOut: + try: + result = place_order( + OrderRequest( + user_id=UserId(payload.user_id), sku=Sku(payload.sku), qty=payload.qty + ), + inventory, + ) + except InvalidQuantity as e: + raise HTTPException(status_code=400, detail=str(e)) from e + except UnknownSku as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except OutOfStock as e: + raise HTTPException(status_code=409, detail=str(e)) from e + + return PlaceOrderOut( + sku=str(result.sku), + qty=result.qty, + remaining_stock=result.remaining_stock, + ) diff --git a/2026/ports/after/adapters/sqlalchemy_inventory.py b/2026/ports/after/adapters/sqlalchemy_inventory.py new file mode 100644 index 00000000..d6faea08 --- /dev/null +++ b/2026/ports/after/adapters/sqlalchemy_inventory.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass + +from sqlalchemy import text +from sqlalchemy.engine import Connection + +from ..domain.errors import UnknownSku +from ..domain.models import Sku +from ..domain.ports import InventoryPort + + +@dataclass +class SqlAlchemyInventoryAdapter(InventoryPort): + conn: Connection + + def get_stock(self, sku: Sku) -> int: + row = self.conn.execute( + text("SELECT stock FROM inventory WHERE sku = :sku"), + {"sku": str(sku)}, + ).fetchone() + + if row is None: + raise UnknownSku(str(sku)) + + return int(row.stock) + + def reserve(self, sku: Sku, qty: int) -> int: + # Demo-friendly (not fully concurrent-safe). Kept here on purpose: infra detail. + self.conn.execute( + text("UPDATE inventory SET stock = stock - :qty WHERE sku = :sku"), + {"sku": str(sku), "qty": qty}, + ) + remaining = self.conn.execute( + text("SELECT stock FROM inventory WHERE sku = :sku"), + {"sku": str(sku)}, + ).scalar_one() + return int(remaining) diff --git a/2026/ports/after/db.py b/2026/ports/after/db.py new file mode 100644 index 00000000..2705323d --- /dev/null +++ b/2026/ports/after/db.py @@ -0,0 +1,25 @@ +from sqlalchemy import create_engine, text +from sqlalchemy.engine import Engine + +DB_URL = "sqlite:///./after.db" +engine: Engine = create_engine(DB_URL, future=True) + + +def init_db() -> None: + with engine.begin() as conn: + conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS inventory ( + sku TEXT PRIMARY KEY, + stock INTEGER NOT NULL + ) + """ + ) + ) + count = conn.execute(text("SELECT COUNT(*) FROM inventory")).scalar_one() + if count == 0: + conn.execute( + text("INSERT INTO inventory(sku, stock) VALUES (:sku, :stock)"), + [{"sku": "ABC", "stock": 10}, {"sku": "XYZ", "stock": 2}], + ) diff --git a/2026/ports/after/domain/__init__.py b/2026/ports/after/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/2026/ports/after/domain/errors.py b/2026/ports/after/domain/errors.py new file mode 100644 index 00000000..b39fffff --- /dev/null +++ b/2026/ports/after/domain/errors.py @@ -0,0 +1,22 @@ +class DomainError(Exception): + """Base class for domain-level errors.""" + + +class InvalidQuantity(DomainError): + pass + + +class UnknownSku(DomainError): + def __init__(self, sku: str) -> None: + super().__init__(f"unknown sku: {sku}") + self.sku = sku + + +class OutOfStock(DomainError): + def __init__(self, sku: str, requested: int, available: int) -> None: + super().__init__( + f"out of stock: {sku}, requested {requested}, available {available}" + ) + self.sku = sku + self.requested = requested + self.available = available diff --git a/2026/ports/after/domain/models.py b/2026/ports/after/domain/models.py new file mode 100644 index 00000000..5927ab08 --- /dev/null +++ b/2026/ports/after/domain/models.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +type UserId = int +type Sku = str + + +@dataclass(frozen=True) +class OrderRequest: + user_id: UserId + sku: Sku + qty: int + + +@dataclass(frozen=True) +class OrderPlaced: + sku: Sku + qty: int + remaining_stock: int diff --git a/2026/ports/after/domain/ports.py b/2026/ports/after/domain/ports.py new file mode 100644 index 00000000..ccfb66ba --- /dev/null +++ b/2026/ports/after/domain/ports.py @@ -0,0 +1,8 @@ +from typing import Protocol + +from .models import Sku + + +class InventoryPort(Protocol): + def get_stock(self, sku: Sku) -> int: ... + def reserve(self, sku: Sku, qty: int) -> int: ... diff --git a/2026/ports/after/domain/use_cases.py b/2026/ports/after/domain/use_cases.py new file mode 100644 index 00000000..cef95b2d --- /dev/null +++ b/2026/ports/after/domain/use_cases.py @@ -0,0 +1,16 @@ +from .errors import InvalidQuantity, OutOfStock +from .models import OrderPlaced, OrderRequest +from .ports import InventoryPort + + +def place_order(req: OrderRequest, inventory: InventoryPort) -> OrderPlaced: + # Domain rule: qty must be positive + if req.qty <= 0: + raise InvalidQuantity("qty must be > 0") + + available = inventory.get_stock(req.sku) + if available < req.qty: + raise OutOfStock(str(req.sku), req.qty, available) + + remaining = inventory.reserve(req.sku, req.qty) + return OrderPlaced(sku=req.sku, qty=req.qty, remaining_stock=remaining) diff --git a/2026/ports/after/main.py b/2026/ports/after/main.py new file mode 100644 index 00000000..011a8eb1 --- /dev/null +++ b/2026/ports/after/main.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from fastapi import FastAPI + +from .adapters import api +from .db import init_db +from .wiring import get_inventory_port + +app = FastAPI(title="AFTER - ports & adapters") + + +@app.on_event("startup") +def on_startup() -> None: + init_db() + + +# Wire the dependency: the API asks for InventoryPort, we provide it via SQLAlchemy adapter. +app.dependency_overrides[api.get_inventory] = get_inventory_port + +app.include_router(api.router) diff --git a/2026/ports/after/wiring.py b/2026/ports/after/wiring.py new file mode 100644 index 00000000..73a754f6 --- /dev/null +++ b/2026/ports/after/wiring.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from collections.abc import Iterator + +from sqlalchemy.engine import Connection + +from .adapters.sqlalchemy_inventory import SqlAlchemyInventoryAdapter +from .db import engine +from .domain.ports import InventoryPort + + +def get_conn() -> Iterator[Connection]: + conn = engine.connect() + try: + yield conn + finally: + conn.close() + + +def get_inventory_port() -> Iterator[InventoryPort]: + # This is the "composition root" for this dependency: + # - open SQLAlchemy connection + # - create the adapter + # - yield it as the port (interface) + for conn in get_conn(): + yield SqlAlchemyInventoryAdapter(conn) diff --git a/2026/ports/before_app.py b/2026/ports/before_app.py new file mode 100644 index 00000000..0b1f4e04 --- /dev/null +++ b/2026/ports/before_app.py @@ -0,0 +1,94 @@ +from typing import Any + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +from sqlalchemy import create_engine, text +from sqlalchemy.engine import Engine + +DB_URL = "sqlite:///./before.db" +engine: Engine = create_engine(DB_URL, future=True) + +app = FastAPI(title="BEFORE - mixed concerns") + + +class PlaceOrderIn(BaseModel): + user_id: int + sku: str + qty: int = Field(..., gt=0) + + +class PlaceOrderOut(BaseModel): + status: str + sku: str + qty: int + remaining_stock: int + + +def init_db() -> None: + """Create table + seed data if needed.""" + with engine.begin() as conn: + conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS inventory ( + sku TEXT PRIMARY KEY, + stock INTEGER NOT NULL + ) + """ + ) + ) + # Seed only if empty + count = conn.execute(text("SELECT COUNT(*) FROM inventory")).scalar_one() + if count == 0: + conn.execute( + text("INSERT INTO inventory(sku, stock) VALUES (:sku, :stock)"), + [{"sku": "ABC", "stock": 10}, {"sku": "XYZ", "stock": 2}], + ) + + +@app.on_event("startup") +def on_startup() -> None: + init_db() + + +def place_order(db_engine: Engine, user_id: int, sku: str, qty: int) -> dict[str, Any]: + """ + ๐Ÿ˜ฌ MIXED: domain rules + DB access + HTTP errors + response shaping. + """ + if qty <= 0: + raise HTTPException(status_code=400, detail="qty must be > 0") + + with db_engine.begin() as conn: + row = conn.execute( + text("SELECT stock FROM inventory WHERE sku=:sku"), + {"sku": sku}, + ).fetchone() + + if row is None: + raise HTTPException(status_code=404, detail="unknown sku") + + available = int(row.stock) + if available < qty: + raise HTTPException( + status_code=409, + detail=f"out of stock: requested {qty}, available {available}", + ) + + # Not fully atomic in a concurrent setting, but ok for demo + conn.execute( + text("UPDATE inventory SET stock = stock - :qty WHERE sku=:sku"), + {"sku": sku, "qty": qty}, + ) + remaining = conn.execute( + text("SELECT stock FROM inventory WHERE sku=:sku"), + {"sku": sku}, + ).scalar_one() + + # API response shaping in the domain-y function + return {"status": "ok", "sku": sku, "qty": qty, "remaining_stock": int(remaining)} + + +@app.post("/orders", response_model=PlaceOrderOut) +def place_order_endpoint(payload: PlaceOrderIn) -> PlaceOrderOut: + result = place_order(engine, payload.user_id, payload.sku, payload.qty) + return PlaceOrderOut(**result) diff --git a/2026/ports/pyproject.toml b/2026/ports/pyproject.toml new file mode 100644 index 00000000..16ad5aaf --- /dev/null +++ b/2026/ports/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "ports" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "fastapi>=0.128.0", + "sqlalchemy>=2.0.45", + "uvicorn>=0.40.0", +] diff --git a/2026/ports/uv.lock b/2026/ports/uv.lock new file mode 100644 index 00000000..07b25d97 --- /dev/null +++ b/2026/ports/uv.lock @@ -0,0 +1,246 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ports" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.128.0" }, + { name = "sqlalchemy", specifier = ">=2.0.45" }, + { name = "uvicorn", specifier = ">=0.40.0" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] From d24d421d5f707e1596edc460de617664522c0773 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 Jan 2026 13:25:33 +0100 Subject: [PATCH 080/113] Updated ports & adapters example. --- 2026/ports/{after => }/adapters/__init__.py | 0 .../adapters/sqlalchemy_inventory.py | 11 +++-- 2026/ports/after/domain/ports.py | 8 ---- 2026/ports/after/main.py | 20 --------- 2026/ports/after/wiring.py | 26 ----------- 2026/ports/{after/adapters => }/api.py | 7 +-- 2026/ports/{before_app.py => before_api.py} | 44 +++---------------- 2026/ports/before_main.py | 13 ++++++ 2026/ports/{after => }/db.py | 7 +-- 2026/ports/{after => }/domain/__init__.py | 0 2026/ports/{after => }/domain/errors.py | 0 2026/ports/{after => }/domain/models.py | 8 +--- 2026/ports/domain/ports.py | 6 +++ 2026/ports/{after => }/domain/use_cases.py | 0 2026/ports/main.py | 26 +++++++++++ 15 files changed, 64 insertions(+), 112 deletions(-) rename 2026/ports/{after => }/adapters/__init__.py (100%) rename 2026/ports/{after => }/adapters/sqlalchemy_inventory.py (78%) delete mode 100644 2026/ports/after/domain/ports.py delete mode 100644 2026/ports/after/main.py delete mode 100644 2026/ports/after/wiring.py rename 2026/ports/{after/adapters => }/api.py (86%) rename 2026/ports/{before_app.py => before_api.py} (54%) create mode 100644 2026/ports/before_main.py rename 2026/ports/{after => }/db.py (85%) rename 2026/ports/{after => }/domain/__init__.py (100%) rename 2026/ports/{after => }/domain/errors.py (100%) rename 2026/ports/{after => }/domain/models.py (68%) create mode 100644 2026/ports/domain/ports.py rename 2026/ports/{after => }/domain/use_cases.py (100%) create mode 100644 2026/ports/main.py diff --git a/2026/ports/after/adapters/__init__.py b/2026/ports/adapters/__init__.py similarity index 100% rename from 2026/ports/after/adapters/__init__.py rename to 2026/ports/adapters/__init__.py diff --git a/2026/ports/after/adapters/sqlalchemy_inventory.py b/2026/ports/adapters/sqlalchemy_inventory.py similarity index 78% rename from 2026/ports/after/adapters/sqlalchemy_inventory.py rename to 2026/ports/adapters/sqlalchemy_inventory.py index d6faea08..06333ca5 100644 --- a/2026/ports/after/adapters/sqlalchemy_inventory.py +++ b/2026/ports/adapters/sqlalchemy_inventory.py @@ -4,7 +4,6 @@ from sqlalchemy.engine import Connection from ..domain.errors import UnknownSku -from ..domain.models import Sku from ..domain.ports import InventoryPort @@ -12,10 +11,10 @@ class SqlAlchemyInventoryAdapter(InventoryPort): conn: Connection - def get_stock(self, sku: Sku) -> int: + def get_stock(self, sku: str) -> int: row = self.conn.execute( text("SELECT stock FROM inventory WHERE sku = :sku"), - {"sku": str(sku)}, + {"sku": sku}, ).fetchone() if row is None: @@ -23,14 +22,14 @@ def get_stock(self, sku: Sku) -> int: return int(row.stock) - def reserve(self, sku: Sku, qty: int) -> int: + def reserve(self, sku: str, qty: int) -> int: # Demo-friendly (not fully concurrent-safe). Kept here on purpose: infra detail. self.conn.execute( text("UPDATE inventory SET stock = stock - :qty WHERE sku = :sku"), - {"sku": str(sku), "qty": qty}, + {"sku": sku, "qty": qty}, ) remaining = self.conn.execute( text("SELECT stock FROM inventory WHERE sku = :sku"), - {"sku": str(sku)}, + {"sku": sku}, ).scalar_one() return int(remaining) diff --git a/2026/ports/after/domain/ports.py b/2026/ports/after/domain/ports.py deleted file mode 100644 index ccfb66ba..00000000 --- a/2026/ports/after/domain/ports.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Protocol - -from .models import Sku - - -class InventoryPort(Protocol): - def get_stock(self, sku: Sku) -> int: ... - def reserve(self, sku: Sku, qty: int) -> int: ... diff --git a/2026/ports/after/main.py b/2026/ports/after/main.py deleted file mode 100644 index 011a8eb1..00000000 --- a/2026/ports/after/main.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from fastapi import FastAPI - -from .adapters import api -from .db import init_db -from .wiring import get_inventory_port - -app = FastAPI(title="AFTER - ports & adapters") - - -@app.on_event("startup") -def on_startup() -> None: - init_db() - - -# Wire the dependency: the API asks for InventoryPort, we provide it via SQLAlchemy adapter. -app.dependency_overrides[api.get_inventory] = get_inventory_port - -app.include_router(api.router) diff --git a/2026/ports/after/wiring.py b/2026/ports/after/wiring.py deleted file mode 100644 index 73a754f6..00000000 --- a/2026/ports/after/wiring.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from collections.abc import Iterator - -from sqlalchemy.engine import Connection - -from .adapters.sqlalchemy_inventory import SqlAlchemyInventoryAdapter -from .db import engine -from .domain.ports import InventoryPort - - -def get_conn() -> Iterator[Connection]: - conn = engine.connect() - try: - yield conn - finally: - conn.close() - - -def get_inventory_port() -> Iterator[InventoryPort]: - # This is the "composition root" for this dependency: - # - open SQLAlchemy connection - # - create the adapter - # - yield it as the port (interface) - for conn in get_conn(): - yield SqlAlchemyInventoryAdapter(conn) diff --git a/2026/ports/after/adapters/api.py b/2026/ports/api.py similarity index 86% rename from 2026/ports/after/adapters/api.py rename to 2026/ports/api.py index 122e9315..558d7bd9 100644 --- a/2026/ports/after/adapters/api.py +++ b/2026/ports/api.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field from ..domain.errors import InvalidQuantity, OutOfStock, UnknownSku -from ..domain.models import OrderRequest, Sku, UserId +from ..domain.models import OrderRequest, Sku from ..domain.ports import InventoryPort from ..domain.use_cases import place_order @@ -10,7 +10,6 @@ class PlaceOrderIn(BaseModel): - user_id: int sku: str qty: int = Field(..., gt=0) @@ -35,9 +34,7 @@ def place_order_endpoint( ) -> PlaceOrderOut: try: result = place_order( - OrderRequest( - user_id=UserId(payload.user_id), sku=Sku(payload.sku), qty=payload.qty - ), + OrderRequest(sku=Sku(payload.sku), qty=payload.qty), inventory, ) except InvalidQuantity as e: diff --git a/2026/ports/before_app.py b/2026/ports/before_api.py similarity index 54% rename from 2026/ports/before_app.py rename to 2026/ports/before_api.py index 0b1f4e04..74931d93 100644 --- a/2026/ports/before_app.py +++ b/2026/ports/before_api.py @@ -1,57 +1,25 @@ from typing import Any -from fastapi import FastAPI, HTTPException +from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field -from sqlalchemy import create_engine, text -from sqlalchemy.engine import Engine -DB_URL = "sqlite:///./before.db" -engine: Engine = create_engine(DB_URL, future=True) +from .db import engine -app = FastAPI(title="BEFORE - mixed concerns") +router = APIRouter() class PlaceOrderIn(BaseModel): - user_id: int sku: str qty: int = Field(..., gt=0) class PlaceOrderOut(BaseModel): - status: str sku: str qty: int remaining_stock: int -def init_db() -> None: - """Create table + seed data if needed.""" - with engine.begin() as conn: - conn.execute( - text( - """ - CREATE TABLE IF NOT EXISTS inventory ( - sku TEXT PRIMARY KEY, - stock INTEGER NOT NULL - ) - """ - ) - ) - # Seed only if empty - count = conn.execute(text("SELECT COUNT(*) FROM inventory")).scalar_one() - if count == 0: - conn.execute( - text("INSERT INTO inventory(sku, stock) VALUES (:sku, :stock)"), - [{"sku": "ABC", "stock": 10}, {"sku": "XYZ", "stock": 2}], - ) - - -@app.on_event("startup") -def on_startup() -> None: - init_db() - - -def place_order(db_engine: Engine, user_id: int, sku: str, qty: int) -> dict[str, Any]: +def place_order(db_engine: Engine, sku: str, qty: int) -> dict[str, Any]: """ ๐Ÿ˜ฌ MIXED: domain rules + DB access + HTTP errors + response shaping. """ @@ -88,7 +56,7 @@ def place_order(db_engine: Engine, user_id: int, sku: str, qty: int) -> dict[str return {"status": "ok", "sku": sku, "qty": qty, "remaining_stock": int(remaining)} -@app.post("/orders", response_model=PlaceOrderOut) +@router.post("/orders", response_model=PlaceOrderOut) def place_order_endpoint(payload: PlaceOrderIn) -> PlaceOrderOut: - result = place_order(engine, payload.user_id, payload.sku, payload.qty) + result = place_order(engine, payload.sku, payload.qty) return PlaceOrderOut(**result) diff --git a/2026/ports/before_main.py b/2026/ports/before_main.py new file mode 100644 index 00000000..531db4b5 --- /dev/null +++ b/2026/ports/before_main.py @@ -0,0 +1,13 @@ +from api import router +from fastapi import FastAPI + +from .db import init_db + +app = FastAPI(title="Ports & adapters") + +app.include_router(router) + + +@app.on_event("startup") +def on_startup() -> None: + init_db("sqlite:///./before.db") diff --git a/2026/ports/after/db.py b/2026/ports/db.py similarity index 85% rename from 2026/ports/after/db.py rename to 2026/ports/db.py index 2705323d..87d63797 100644 --- a/2026/ports/after/db.py +++ b/2026/ports/db.py @@ -1,11 +1,12 @@ from sqlalchemy import create_engine, text from sqlalchemy.engine import Engine -DB_URL = "sqlite:///./after.db" -engine: Engine = create_engine(DB_URL, future=True) +engine: Engine -def init_db() -> None: +def init_db(db_url: str) -> None: + global engine + engine = create_engine(db_url, future=True) with engine.begin() as conn: conn.execute( text( diff --git a/2026/ports/after/domain/__init__.py b/2026/ports/domain/__init__.py similarity index 100% rename from 2026/ports/after/domain/__init__.py rename to 2026/ports/domain/__init__.py diff --git a/2026/ports/after/domain/errors.py b/2026/ports/domain/errors.py similarity index 100% rename from 2026/ports/after/domain/errors.py rename to 2026/ports/domain/errors.py diff --git a/2026/ports/after/domain/models.py b/2026/ports/domain/models.py similarity index 68% rename from 2026/ports/after/domain/models.py rename to 2026/ports/domain/models.py index 5927ab08..c15bbd69 100644 --- a/2026/ports/after/domain/models.py +++ b/2026/ports/domain/models.py @@ -1,18 +1,14 @@ from dataclasses import dataclass -type UserId = int -type Sku = str - @dataclass(frozen=True) class OrderRequest: - user_id: UserId - sku: Sku + sku: str qty: int @dataclass(frozen=True) class OrderPlaced: - sku: Sku + sku: str qty: int remaining_stock: int diff --git a/2026/ports/domain/ports.py b/2026/ports/domain/ports.py new file mode 100644 index 00000000..e8abc68c --- /dev/null +++ b/2026/ports/domain/ports.py @@ -0,0 +1,6 @@ +from typing import Protocol + + +class InventoryPort(Protocol): + def get_stock(self, sku: str) -> int: ... + def reserve(self, sku: str, qty: int) -> int: ... diff --git a/2026/ports/after/domain/use_cases.py b/2026/ports/domain/use_cases.py similarity index 100% rename from 2026/ports/after/domain/use_cases.py rename to 2026/ports/domain/use_cases.py diff --git a/2026/ports/main.py b/2026/ports/main.py new file mode 100644 index 00000000..9f4c55bf --- /dev/null +++ b/2026/ports/main.py @@ -0,0 +1,26 @@ +from api import get_inventory, router +from fastapi import FastAPI + +from .adapters.sqlalchemy_inventory import SqlAlchemyInventoryAdapter +from .db import engine, init_db + +app = FastAPI(title="Ports & adapters") + + +def get_inventory_port(): + conn = engine.connect() + try: + yield SqlAlchemyInventoryAdapter(conn) + finally: + conn.close() + + +@app.on_event("startup") +def on_startup() -> None: + init_db("sqlite:///./after.db") + + +# Wire the dependency +app.dependency_overrides[get_inventory] = get_inventory_port + +app.include_router(router) From 7ddb1517e367156a4312cbf6ea98b3c7fb4e3e17 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 28 Jan 2026 14:54:40 +0100 Subject: [PATCH 081/113] Example cleanup --- 2026/ports/api.py | 26 +++++++--------- 2026/ports/before_api.py | 63 ++++++++++++++++++++------------------- 2026/ports/before_main.py | 13 -------- 2026/ports/main.py | 19 ++---------- 4 files changed, 46 insertions(+), 75 deletions(-) delete mode 100644 2026/ports/before_main.py diff --git a/2026/ports/api.py b/2026/ports/api.py index 558d7bd9..722adc07 100644 --- a/2026/ports/api.py +++ b/2026/ports/api.py @@ -1,10 +1,11 @@ +from adapters.sqlalchemy_inventory import SqlAlchemyInventoryAdapter +from db import get_db +from domain.errors import InvalidQuantity, OutOfStock, UnknownSku +from domain.models import OrderRequest +from domain.use_cases import place_order from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field - -from ..domain.errors import InvalidQuantity, OutOfStock, UnknownSku -from ..domain.models import OrderRequest, Sku -from ..domain.ports import InventoryPort -from ..domain.use_cases import place_order +from sqlalchemy.engine import Connection router = APIRouter() @@ -20,22 +21,15 @@ class PlaceOrderOut(BaseModel): remaining_stock: int -def get_inventory() -> InventoryPort: - """ - Stub for FastAPI DI. The real provider is wired in main.py. - """ - raise NotImplementedError - - @router.post("/orders", response_model=PlaceOrderOut) def place_order_endpoint( payload: PlaceOrderIn, - inventory: InventoryPort = Depends(get_inventory), + connection: Connection = Depends(get_db), ) -> PlaceOrderOut: try: result = place_order( - OrderRequest(sku=Sku(payload.sku), qty=payload.qty), - inventory, + OrderRequest(**payload.model_dump()), + SqlAlchemyInventoryAdapter(conn=connection), ) except InvalidQuantity as e: raise HTTPException(status_code=400, detail=str(e)) from e @@ -45,7 +39,7 @@ def place_order_endpoint( raise HTTPException(status_code=409, detail=str(e)) from e return PlaceOrderOut( - sku=str(result.sku), + sku=result.sku, qty=result.qty, remaining_stock=result.remaining_stock, ) diff --git a/2026/ports/before_api.py b/2026/ports/before_api.py index 74931d93..2c025a16 100644 --- a/2026/ports/before_api.py +++ b/2026/ports/before_api.py @@ -1,9 +1,10 @@ from typing import Any -from fastapi import APIRouter, HTTPException +from db import get_db +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field - -from .db import engine +from sqlalchemy import text +from sqlalchemy.engine import Connection router = APIRouter() @@ -19,44 +20,46 @@ class PlaceOrderOut(BaseModel): remaining_stock: int -def place_order(db_engine: Engine, sku: str, qty: int) -> dict[str, Any]: +def place_order(db: Connection, sku: str, qty: int) -> dict[str, Any]: """ ๐Ÿ˜ฌ MIXED: domain rules + DB access + HTTP errors + response shaping. """ if qty <= 0: raise HTTPException(status_code=400, detail="qty must be > 0") - with db_engine.begin() as conn: - row = conn.execute( - text("SELECT stock FROM inventory WHERE sku=:sku"), - {"sku": sku}, - ).fetchone() - - if row is None: - raise HTTPException(status_code=404, detail="unknown sku") - - available = int(row.stock) - if available < qty: - raise HTTPException( - status_code=409, - detail=f"out of stock: requested {qty}, available {available}", - ) - - # Not fully atomic in a concurrent setting, but ok for demo - conn.execute( - text("UPDATE inventory SET stock = stock - :qty WHERE sku=:sku"), - {"sku": sku, "qty": qty}, + row = db.execute( + text("SELECT stock FROM inventory WHERE sku=:sku"), + {"sku": sku}, + ).fetchone() + + if row is None: + raise HTTPException(status_code=404, detail="unknown sku") + + available = int(row.stock) + if available < qty: + raise HTTPException( + status_code=409, + detail=f"out of stock: requested {qty}, available {available}", ) - remaining = conn.execute( - text("SELECT stock FROM inventory WHERE sku=:sku"), - {"sku": sku}, - ).scalar_one() + + # Not fully atomic in a concurrent setting, but ok for demo + db.execute( + text("UPDATE inventory SET stock = stock - :qty WHERE sku=:sku"), + {"sku": sku, "qty": qty}, + ) + remaining = db.execute( + text("SELECT stock FROM inventory WHERE sku=:sku"), + {"sku": sku}, + ).scalar_one() # API response shaping in the domain-y function return {"status": "ok", "sku": sku, "qty": qty, "remaining_stock": int(remaining)} @router.post("/orders", response_model=PlaceOrderOut) -def place_order_endpoint(payload: PlaceOrderIn) -> PlaceOrderOut: - result = place_order(engine, payload.sku, payload.qty) +def place_order_endpoint( + payload: PlaceOrderIn, + connection: Connection = Depends(get_db), +) -> PlaceOrderOut: + result = place_order(connection, payload.sku, payload.qty) return PlaceOrderOut(**result) diff --git a/2026/ports/before_main.py b/2026/ports/before_main.py deleted file mode 100644 index 531db4b5..00000000 --- a/2026/ports/before_main.py +++ /dev/null @@ -1,13 +0,0 @@ -from api import router -from fastapi import FastAPI - -from .db import init_db - -app = FastAPI(title="Ports & adapters") - -app.include_router(router) - - -@app.on_event("startup") -def on_startup() -> None: - init_db("sqlite:///./before.db") diff --git a/2026/ports/main.py b/2026/ports/main.py index 9f4c55bf..690f1ff4 100644 --- a/2026/ports/main.py +++ b/2026/ports/main.py @@ -1,26 +1,13 @@ -from api import get_inventory, router +from api import router +from db import init_db from fastapi import FastAPI -from .adapters.sqlalchemy_inventory import SqlAlchemyInventoryAdapter -from .db import engine, init_db - app = FastAPI(title="Ports & adapters") -def get_inventory_port(): - conn = engine.connect() - try: - yield SqlAlchemyInventoryAdapter(conn) - finally: - conn.close() - - @app.on_event("startup") def on_startup() -> None: - init_db("sqlite:///./after.db") - + init_db("sqlite:///./before.db") -# Wire the dependency -app.dependency_overrides[get_inventory] = get_inventory_port app.include_router(router) From de21d26e4f134e2e88240e4cb5c7c9c8fc677b03 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 28 Jan 2026 16:01:40 +0100 Subject: [PATCH 082/113] Minor improvements. --- 2026/ports/adapters/sqlalchemy_inventory.py | 19 ++++++++++++------- 2026/ports/domain/ports.py | 1 + 2026/ports/domain/use_cases.py | 10 ++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/2026/ports/adapters/sqlalchemy_inventory.py b/2026/ports/adapters/sqlalchemy_inventory.py index 06333ca5..d0d3b7c8 100644 --- a/2026/ports/adapters/sqlalchemy_inventory.py +++ b/2026/ports/adapters/sqlalchemy_inventory.py @@ -1,29 +1,31 @@ from dataclasses import dataclass +from domain.ports import InventoryPort from sqlalchemy import text from sqlalchemy.engine import Connection -from ..domain.errors import UnknownSku -from ..domain.ports import InventoryPort - @dataclass class SqlAlchemyInventoryAdapter(InventoryPort): conn: Connection + def exists_sku(self, sku: str) -> bool: + row = self.conn.execute( + text("SELECT 1 FROM inventory WHERE sku = :sku"), + {"sku": sku}, + ).fetchone() + + return row is not None + def get_stock(self, sku: str) -> int: row = self.conn.execute( text("SELECT stock FROM inventory WHERE sku = :sku"), {"sku": sku}, ).fetchone() - if row is None: - raise UnknownSku(str(sku)) - return int(row.stock) def reserve(self, sku: str, qty: int) -> int: - # Demo-friendly (not fully concurrent-safe). Kept here on purpose: infra detail. self.conn.execute( text("UPDATE inventory SET stock = stock - :qty WHERE sku = :sku"), {"sku": sku, "qty": qty}, @@ -32,4 +34,7 @@ def reserve(self, sku: str, qty: int) -> int: text("SELECT stock FROM inventory WHERE sku = :sku"), {"sku": sku}, ).scalar_one() + + self.conn.commit() + return int(remaining) diff --git a/2026/ports/domain/ports.py b/2026/ports/domain/ports.py index e8abc68c..3b15057a 100644 --- a/2026/ports/domain/ports.py +++ b/2026/ports/domain/ports.py @@ -2,5 +2,6 @@ class InventoryPort(Protocol): + def exists_sku(self, sku: str) -> bool: ... def get_stock(self, sku: str) -> int: ... def reserve(self, sku: str, qty: int) -> int: ... diff --git a/2026/ports/domain/use_cases.py b/2026/ports/domain/use_cases.py index cef95b2d..a3f8eba7 100644 --- a/2026/ports/domain/use_cases.py +++ b/2026/ports/domain/use_cases.py @@ -1,16 +1,18 @@ -from .errors import InvalidQuantity, OutOfStock +from .errors import InvalidQuantity, OutOfStock, UnknownSku from .models import OrderPlaced, OrderRequest from .ports import InventoryPort def place_order(req: OrderRequest, inventory: InventoryPort) -> OrderPlaced: - # Domain rule: qty must be positive if req.qty <= 0: - raise InvalidQuantity("qty must be > 0") + raise InvalidQuantity() + + if not inventory.exists_sku(req.sku): + raise UnknownSku(req.sku) available = inventory.get_stock(req.sku) if available < req.qty: - raise OutOfStock(str(req.sku), req.qty, available) + raise OutOfStock(req.sku, req.qty, available) remaining = inventory.reserve(req.sku, req.qty) return OrderPlaced(sku=req.sku, qty=req.qty, remaining_stock=remaining) From 5c2c72f8ef4dedaa989623025aa404194bf4d69e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 22 Jan 2026 13:58:48 +0100 Subject: [PATCH 083/113] Added code example --- 2026/dctricks/0_initvar_teaser.py | 7 +++ 2026/dctricks/1_singleton.py | 32 ++++++++++++ 2026/dctricks/2_autoregistry.py | 28 +++++++++++ 2026/dctricks/3_validation.py | 78 ++++++++++++++++++++++++++++++ 2026/dctricks/4_sqlgen.py | 49 +++++++++++++++++++ 2026/dctricks/5_cached_prop.py | 35 ++++++++++++++ 2026/dctricks/6_cli.py | 35 ++++++++++++++ 2026/dctricks/7_context_manager.py | 27 +++++++++++ 2026/dctricks/pyproject.toml | 6 +++ 9 files changed, 297 insertions(+) create mode 100644 2026/dctricks/0_initvar_teaser.py create mode 100644 2026/dctricks/1_singleton.py create mode 100644 2026/dctricks/2_autoregistry.py create mode 100644 2026/dctricks/3_validation.py create mode 100644 2026/dctricks/4_sqlgen.py create mode 100644 2026/dctricks/5_cached_prop.py create mode 100644 2026/dctricks/6_cli.py create mode 100644 2026/dctricks/7_context_manager.py create mode 100644 2026/dctricks/pyproject.toml diff --git a/2026/dctricks/0_initvar_teaser.py b/2026/dctricks/0_initvar_teaser.py new file mode 100644 index 00000000..4ea4e698 --- /dev/null +++ b/2026/dctricks/0_initvar_teaser.py @@ -0,0 +1,7 @@ +from dataclasses import InitVar, dataclass + + +@dataclass +class User: + email: str + raw_password: InitVar[str] diff --git a/2026/dctricks/1_singleton.py b/2026/dctricks/1_singleton.py new file mode 100644 index 00000000..3d9eda77 --- /dev/null +++ b/2026/dctricks/1_singleton.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import ClassVar, Self + + +class EnvSingleton: + _instances: ClassVar[dict[str, Self]] = {} + + def __new__(cls, env: str, *args: object, **kwargs: object) -> Self: + if env not in cls._instances: + cls._instances[env] = super().__new__(cls) + return cls._instances[env] + + +@dataclass +class Config(EnvSingleton): + env: str + debug: bool + + +def main() -> None: + a = Config("prod", False) + b = Config("prod", True) + c = Config("dev", True) + + print(a is b) + print(a.debug) + print(a is c) + print(c.debug) + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/2_autoregistry.py b/2026/dctricks/2_autoregistry.py new file mode 100644 index 00000000..b41a63a7 --- /dev/null +++ b/2026/dctricks/2_autoregistry.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Self + + +class Event: + registry: dict[str, type[Self]] = {} + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + Event.registry[cls.__name__] = cls + + +@dataclass +class UserCreated(Event): + user_id: int + + +@dataclass +class UserDeleted(Event): + user_id: int + + +def main() -> None: + print(Event.registry) + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/3_validation.py b/2026/dctricks/3_validation.py new file mode 100644 index 00000000..8a9bc67e --- /dev/null +++ b/2026/dctricks/3_validation.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass +from typing import Any, Callable, Final, Iterator, Tuple + +VALIDATOR_ATTR: Final[str] = "_validate_field" + + +def validator(field_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + setattr(func, VALIDATOR_ATTR, field_name) + return func + + return decorator + + +class Validatable: + """ + Base class for dataclasses that want field-level validation/transforms. + + Contract: + - Validators are instance methods decorated with @validator(""). + - Validator methods take the current field value and return the new value. + """ + + def __post_init__(self) -> None: + for field_name, validate in self._validators(): + if not hasattr(self, field_name): + raise AttributeError( + f"{self.__class__.__name__} has validator for unknown field '{field_name}'" + ) + + value = getattr(self, field_name) + new_value = validate(value) + setattr(self, field_name, new_value) + + def _validators(self) -> Iterator[Tuple[str, Callable[[Any], Any]]]: + """ + Yield (field_name, validator_method) for every validator defined on this instance. + """ + for attr_name in dir(self): + method = getattr(self, attr_name) + if not callable(method): + continue + + field_name = getattr(method, VALIDATOR_ATTR, None) + if field_name is None: + continue + + # At this point it's a validator. We expect it to be a bound instance method: + # Callable[[Any], Any] + yield field_name, method # type: ignore[misc] + + +@dataclass +class User(Validatable): + name: str + age: int + + @validator("age") + def validate_age(self, value: int) -> int: + if value < 0: + raise ValueError("Age cannot be negative") + return value + + @validator("name") + def validate_name(self, value: str) -> str: + return value.strip().title() + + +def main() -> None: + print(User(" alice ", 30)) + try: + print(User("bob", -5)) + except ValueError as e: + print(f"Caught error as expected: {e}") + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/4_sqlgen.py b/2026/dctricks/4_sqlgen.py new file mode 100644 index 00000000..baa096af --- /dev/null +++ b/2026/dctricks/4_sqlgen.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass, field, fields +from typing import Any, ClassVar, Optional, Protocol + + +@dataclass +class UserRow: + id: int = field(metadata={"pk": True}) + email: str + age: Optional[int] = None + + +class DataclassLike(Protocol): + __dataclass_fields__: ClassVar[dict[str, Any]] + + +def to_sql_schema(cls: type[DataclassLike]) -> str: + type_map: dict[type[Any], str] = { + int: "INTEGER", + str: "TEXT", + } + + columns: list[str] = [] + + for f in fields(cls): + print(f.type) + base = f.type if isinstance(f.type, type) else None + sql_type = type_map.get(base, "TEXT") if base is not None else "TEXT" + column = f"{f.name} {sql_type}" + + if f.metadata.get("pk"): + column += " PRIMARY KEY" + + if f.default is None: + column += " NULL" + else: + column += " NOT NULL" + + columns.append(column) + + table = cls.__name__.lower() + return f"CREATE TABLE {table} (\n " + ",\n ".join(columns) + "\n);" + + +def main() -> None: + print(to_sql_schema(UserRow)) + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/5_cached_prop.py b/2026/dctricks/5_cached_prop.py new file mode 100644 index 00000000..2b6119ec --- /dev/null +++ b/2026/dctricks/5_cached_prop.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from urllib.parse import urlparse + + +@dataclass(frozen=True) +class Endpoint: + url: str + + @cached_property + def parsed(self): + # cached_property works on normal classes; frozen dataclass is fine because + # cached_property stores on the instance via object.__setattr__ internally. + return urlparse(self.url) + + @property + def host(self) -> str: + return self.parsed.hostname or "" + + @property + def is_https(self) -> bool: + return self.parsed.scheme == "https" + + +def main() -> None: + e = Endpoint("https://arjan.codes/designguide") + print(e.host) + print(e.is_https) + print(e.parsed) # computed once, then cached + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/6_cli.py b/2026/dctricks/6_cli.py new file mode 100644 index 00000000..e5dad669 --- /dev/null +++ b/2026/dctricks/6_cli.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import argparse +from dataclasses import dataclass, fields +from typing import Self, Type, TypeVar + +T = TypeVar("T", bound="CLIArgs") + + +@dataclass +class CLIArgs: + @classmethod + def from_command_line(cls: Type[Self]) -> Self: + parser = argparse.ArgumentParser() + + for f in fields(cls): + arg_name = f"--{f.name.replace('_', '-')}" + if f.type is bool: + parser.add_argument(arg_name, action="store_true") + else: + parser.add_argument(arg_name, type=f.type, default=f.default) + + parsed = parser.parse_args() + return cls(**vars(parsed)) + + +@dataclass +class Args(CLIArgs): + verbose: bool = False + filename: str = "data.txt" + retries: int = 3 + + +args = Args.from_command_line() +print(args) diff --git a/2026/dctricks/7_context_manager.py b/2026/dctricks/7_context_manager.py new file mode 100644 index 00000000..b19c0889 --- /dev/null +++ b/2026/dctricks/7_context_manager.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import IO, Optional + + +@dataclass +class FileResource: + path: str + mode: str = "r" + file: Optional[IO[str]] = None + + def __enter__(self) -> FileResource: + print(f"Opening {self.path}") + self.file = open(self.path, self.mode, encoding="utf-8") + return self + + def __exit__(self, exc_type: object, exc: object, tb: object) -> None: + print(f"Closing {self.path}") + if self.file is not None: + self.file.close() + + +with FileResource("example.txt", "w") as res: + assert res.file is not None + res.file.write("Hello world!") + print(res.path, res.mode) diff --git a/2026/dctricks/pyproject.toml b/2026/dctricks/pyproject.toml new file mode 100644 index 00000000..4a5ec2b5 --- /dev/null +++ b/2026/dctricks/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "dctricks" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] From aeb546f6b837ee89a1f189e7b34190c7c8d29923 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 Jan 2026 10:00:54 +0100 Subject: [PATCH 084/113] Improved Singleton example --- 2026/dctricks/1_singleton.py | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/2026/dctricks/1_singleton.py b/2026/dctricks/1_singleton.py index 3d9eda77..c8958f5c 100644 --- a/2026/dctricks/1_singleton.py +++ b/2026/dctricks/1_singleton.py @@ -2,30 +2,31 @@ from typing import ClassVar, Self -class EnvSingleton: - _instances: ClassVar[dict[str, Self]] = {} - - def __new__(cls, env: str, *args: object, **kwargs: object) -> Self: - if env not in cls._instances: - cls._instances[env] = super().__new__(cls) - return cls._instances[env] +@dataclass(frozen=True, slots=True) +class Config: + env: str + debug: bool = False + _cache: ClassVar[dict[str, Self]] = {} -@dataclass -class Config(EnvSingleton): - env: str - debug: bool + @classmethod + def for_env(cls, env: str, debug: bool = False) -> Self: + # First call wins for a given env + if env not in cls._cache: + cls._cache[env] = cls(env=env, debug=debug) + return cls._cache[env] def main() -> None: - a = Config("prod", False) - b = Config("prod", True) - c = Config("dev", True) - - print(a is b) - print(a.debug) - print(a is c) - print(c.debug) + a = Config.for_env("prod", debug=True) + b = Config.for_env("prod") # does not reset debug + c = Config.for_env("dev", debug=True) + + print(a is b) # True + print(a.debug) # True + print(b.debug) # True + print(a is c) # False + print(c.debug) # True if __name__ == "__main__": From 242aa18a13a82fe4b0f28ef21fc7d8783a83a3fe Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 Jan 2026 10:17:51 +0100 Subject: [PATCH 085/113] More example updates. --- 2026/dctricks/6_cli.py | 15 ++++++++------- 2026/dctricks/7_context_manager.py | 15 +++++++++------ 2026/dctricks/8_initvar.py | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 2026/dctricks/8_initvar.py diff --git a/2026/dctricks/6_cli.py b/2026/dctricks/6_cli.py index e5dad669..5ee0dfcd 100644 --- a/2026/dctricks/6_cli.py +++ b/2026/dctricks/6_cli.py @@ -1,10 +1,6 @@ -from __future__ import annotations - import argparse from dataclasses import dataclass, fields -from typing import Self, Type, TypeVar - -T = TypeVar("T", bound="CLIArgs") +from typing import Self, Type @dataclass @@ -31,5 +27,10 @@ class Args(CLIArgs): retries: int = 3 -args = Args.from_command_line() -print(args) +def main() -> None: + args = Args.from_command_line() + print(args) + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/7_context_manager.py b/2026/dctricks/7_context_manager.py index b19c0889..c7c540d0 100644 --- a/2026/dctricks/7_context_manager.py +++ b/2026/dctricks/7_context_manager.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from dataclasses import dataclass from typing import IO, Optional @@ -21,7 +19,12 @@ def __exit__(self, exc_type: object, exc: object, tb: object) -> None: self.file.close() -with FileResource("example.txt", "w") as res: - assert res.file is not None - res.file.write("Hello world!") - print(res.path, res.mode) +def main() -> None: + with FileResource("example.txt", "w") as res: + assert res.file is not None + res.file.write("Hello world!") + print(res.path, res.mode) + + +if __name__ == "__main__": + main() diff --git a/2026/dctricks/8_initvar.py b/2026/dctricks/8_initvar.py new file mode 100644 index 00000000..a437b4b5 --- /dev/null +++ b/2026/dctricks/8_initvar.py @@ -0,0 +1,23 @@ +from dataclasses import InitVar, dataclass + + +@dataclass +class UserWithPassword: + email: str + raw_password: InitVar[str] + password_hash: int = 0 + + def __post_init__(self, raw_password: str) -> None: + self.password_hash = hash(raw_password) + + +def main() -> None: + u = UserWithPassword("alice@test.com", "super-secret") + + print(u.email) + print(u.password_hash) + # print(u.raw_password) # AttributeError + + +if __name__ == "__main__": + main() From 49599a93b1f8e1b9afdd478dbec95d8b5cd62c9f Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 Jan 2026 10:18:43 +0100 Subject: [PATCH 086/113] Removed Optional type --- 2026/dctricks/7_context_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2026/dctricks/7_context_manager.py b/2026/dctricks/7_context_manager.py index c7c540d0..d5d02d29 100644 --- a/2026/dctricks/7_context_manager.py +++ b/2026/dctricks/7_context_manager.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from typing import IO, Optional +from typing import IO @dataclass class FileResource: path: str mode: str = "r" - file: Optional[IO[str]] = None + file: IO[str] | None = None def __enter__(self) -> FileResource: print(f"Opening {self.path}") From 2c999689ef5ae7fb86f30e59c38219090dd9c295 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 27 Jan 2026 14:13:12 +0100 Subject: [PATCH 087/113] Improved auto registry example. --- 2026/dctricks/2_autoregistry.py | 25 ++++++++++++++----------- 2026/dctricks/5_cached_prop.py | 2 -- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/2026/dctricks/2_autoregistry.py b/2026/dctricks/2_autoregistry.py index b41a63a7..36380e02 100644 --- a/2026/dctricks/2_autoregistry.py +++ b/2026/dctricks/2_autoregistry.py @@ -1,27 +1,30 @@ from dataclasses import dataclass -from typing import Self +from typing import Any, dataclass_transform +REGISTRY: dict[str, type[Any]] = {} -class Event: - registry: dict[str, type[Self]] = {} - def __init_subclass__(cls, **kwargs: object) -> None: - super().__init_subclass__(**kwargs) - Event.registry[cls.__name__] = cls +@dataclass_transform() +def event[T](cls: type[T]) -> type[T]: + dc_cls = dataclass(cls) + REGISTRY[cls.__name__] = dc_cls + return dc_cls -@dataclass -class UserCreated(Event): +@event +class UserCreated: user_id: int -@dataclass -class UserDeleted(Event): +@event +class UserDeleted: user_id: int def main() -> None: - print(Event.registry) + print(REGISTRY) + e = UserCreated(123) + print(e) if __name__ == "__main__": diff --git a/2026/dctricks/5_cached_prop.py b/2026/dctricks/5_cached_prop.py index 2b6119ec..b6f718cb 100644 --- a/2026/dctricks/5_cached_prop.py +++ b/2026/dctricks/5_cached_prop.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from dataclasses import dataclass from functools import cached_property from urllib.parse import urlparse From 3168342070937d75210a42cbb3a8adfc6fc10ae0 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 23 Jan 2026 14:17:55 +0100 Subject: [PATCH 088/113] Added value object example. --- 2026/value/after_price.py | 47 +++++++++++++++++++++++++++++++++++++ 2026/value/before_price.py | 25 ++++++++++++++++++++ 2026/value/email_address.py | 28 ++++++++++++++++++++++ 2026/value/pyproject.toml | 6 +++++ 4 files changed, 106 insertions(+) create mode 100644 2026/value/after_price.py create mode 100644 2026/value/before_price.py create mode 100644 2026/value/email_address.py create mode 100644 2026/value/pyproject.toml diff --git a/2026/value/after_price.py b/2026/value/after_price.py new file mode 100644 index 00000000..6620fef0 --- /dev/null +++ b/2026/value/after_price.py @@ -0,0 +1,47 @@ +from typing import Any, Self + + +class Price(float): + """Validated float representing a non-negative price.""" + + def __new__(cls, value: Any) -> Self: + val = float(value) + if val < 0: + raise ValueError("Price must be non-negative") + return super().__new__(cls, val) + + +class Percentage(float): + """Validated float representing a fraction between 0 and 1.""" + + def __new__(cls, value: Any) -> Self: + val = float(value) + if not 0.0 <= val <= 1.0: + raise ValueError("Percentage must be between 0 and 1") + return super().__new__(cls, val) + + @classmethod + def from_percent(cls, value: Any) -> Self: + return cls(float(value) / 100.0) + + +def apply_discount(price: Price, discount: Percentage) -> Price: + return Price(price * (1.0 - discount)) + + +def main() -> None: + price = Price(100.0) + + # Explicit and safe + discount = Percentage.from_percent(20) + discounted = apply_discount(price, discount) + print(discounted) # 80.0 + + # All of these now fail early and loudly: + # Price(-50) + # Percentage(20) + # apply_discount(price, Percentage(-0.1)) + + +if __name__ == "__main__": + main() diff --git a/2026/value/before_price.py b/2026/value/before_price.py new file mode 100644 index 00000000..362e5220 --- /dev/null +++ b/2026/value/before_price.py @@ -0,0 +1,25 @@ +def apply_discount(price: float, discount: float) -> float: + # Caller must remember: + # - price >= 0 + # - discount is a fraction between 0 and 1 + return price * (1.0 - discount) + + +def main() -> None: + price = 100.0 + + # Works, but relies on discipline + discounted = apply_discount(price, 0.2) + print(discounted) # 80.0 + + # Silent bug: discount meant as "20%" + discounted_wrong = apply_discount(price, 20) + print(discounted_wrong) # -1900.0 ๐Ÿ˜ฌ + + # Negative prices are also allowed + negative = apply_discount(-50.0, 0.1) + print(negative) # -45.0 ๐Ÿ˜ฌ + + +if __name__ == "__main__": + main() diff --git a/2026/value/email_address.py b/2026/value/email_address.py new file mode 100644 index 00000000..ed2ce534 --- /dev/null +++ b/2026/value/email_address.py @@ -0,0 +1,28 @@ +import re +from dataclasses import dataclass + +EMAIL_RE = re.compile(r"^[^@]+@[^@]+\.[^@]+$") + + +@dataclass(frozen=True) +class EmailAddress: + value: str + + def __post_init__(self) -> None: + if not EMAIL_RE.match(self.value): + raise ValueError(f"Invalid email address: {self.value}") + + @property + def domain(self) -> str: + return self.value.split("@", 1)[1] + + +def main() -> None: + email = EmailAddress("hello@example.com") + print(email.domain) # example.com + + # EmailAddress("not-an-email") # raises ValueError + + +if __name__ == "__main__": + main() diff --git a/2026/value/pyproject.toml b/2026/value/pyproject.toml new file mode 100644 index 00000000..2cc33ab2 --- /dev/null +++ b/2026/value/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "value" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] From f19e88e931acb0e667b31790f0b6fdde31579aab Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 16 Jan 2026 17:12:00 +0100 Subject: [PATCH 089/113] Added property vs method code example --- 2026/props/pyproject.toml | 6 ++ 2026/props/simple.py | 46 ++++++++++++++++ 2026/props/user_repo.py | 112 ++++++++++++++++++++++++++++++++++++++ 2026/props/uv.lock | 8 +++ 2026/props/with_async.py | 97 +++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+) create mode 100644 2026/props/pyproject.toml create mode 100644 2026/props/simple.py create mode 100644 2026/props/user_repo.py create mode 100644 2026/props/uv.lock create mode 100644 2026/props/with_async.py diff --git a/2026/props/pyproject.toml b/2026/props/pyproject.toml new file mode 100644 index 00000000..4ac4ed52 --- /dev/null +++ b/2026/props/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "props" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/props/simple.py b/2026/props/simple.py new file mode 100644 index 00000000..6999e938 --- /dev/null +++ b/2026/props/simple.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass + +from user_repo import AccountStatus + + +@dataclass +class UserAccount: + user_id: int + _username: str + _email: str + _status: AccountStatus + + # ---- Methods for everything ---- + + def get_username(self) -> str: + return self._username + + def get_email(self) -> str: + return self._email + + def get_status(self) -> AccountStatus: + return self._status + + def is_active(self) -> bool: + return self.get_status() is AccountStatus.ACTIVE + + +# ---- Demo ---- + + +def main() -> None: + account = UserAccount( + user_id=101, + _username="mason", + _email="mason@arjancodes.com", + _status=AccountStatus.ACTIVE, + ) + + print("Username:", account.get_username()) + print("Email:", account.get_email()) + print("Status:", account.get_status()) + print("Is active:", account.is_active()) + + +if __name__ == "__main__": + main() diff --git a/2026/props/user_repo.py b/2026/props/user_repo.py new file mode 100644 index 00000000..ece23b78 --- /dev/null +++ b/2026/props/user_repo.py @@ -0,0 +1,112 @@ +import asyncio +from dataclasses import dataclass, replace +from enum import StrEnum +from typing import Protocol + + +class AccountStatus(StrEnum): + ACTIVE = "active" + SUSPENDED = "suspended" + CLOSED = "closed" + + +@dataclass(frozen=True) +class UserRecord: + username: str + email: str + status: AccountStatus + + +# ---- Repository abstraction ---- + + +class UserRepository(Protocol): + async def fetch_username(self, user_id: int) -> str: ... + async def fetch_email(self, user_id: int) -> str: ... + async def fetch_status(self, user_id: int) -> AccountStatus: ... + + async def update_username(self, user_id: int, username: str) -> None: ... + async def update_email(self, user_id: int, email: str) -> None: ... + async def update_status(self, user_id: int, status: AccountStatus) -> None: ... + + async def create_user( + self, + user_id: int, + *, + username: str, + email: str, + status: AccountStatus, + ) -> None: ... + + async def delete_user(self, user_id: int) -> None: ... + + +# ---- Concrete repository (in-memory, async) ---- + + +class InMemoryUserRepository: + def __init__(self) -> None: + self._users: dict[int, UserRecord] = { + 101: UserRecord( + username="mason", + email="mason@arjancodes.com", + status=AccountStatus.ACTIVE, + ), + 204: UserRecord( + username="harper", + email="harper@arjancodes.com", + status=AccountStatus.SUSPENDED, + ), + } + + async def fetch_username(self, user_id: int) -> str: + await asyncio.sleep(0.05) + return self._users[user_id].username + + async def fetch_email(self, user_id: int) -> str: + await asyncio.sleep(0.05) + return self._users[user_id].email + + async def fetch_status(self, user_id: int) -> AccountStatus: + await asyncio.sleep(0.05) + return self._users[user_id].status + + async def update_username(self, user_id: int, username: str) -> None: + await asyncio.sleep(0.05) + self._users[user_id] = replace( + self._users[user_id], + username=username, + ) + + async def update_email(self, user_id: int, email: str) -> None: + await asyncio.sleep(0.05) + self._users[user_id] = replace( + self._users[user_id], + email=email, + ) + + async def update_status(self, user_id: int, status: AccountStatus) -> None: + await asyncio.sleep(0.05) + self._users[user_id] = replace( + self._users[user_id], + status=status, + ) + + async def create_user( + self, + user_id: int, + *, + username: str, + email: str, + status: AccountStatus, + ) -> None: + await asyncio.sleep(0.05) + self._users[user_id] = UserRecord( + username=username, + email=email, + status=status, + ) + + async def delete_user(self, user_id: int) -> None: + await asyncio.sleep(0.05) + del self._users[user_id] diff --git a/2026/props/uv.lock b/2026/props/uv.lock new file mode 100644 index 00000000..992438c2 --- /dev/null +++ b/2026/props/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "props" +version = "0.0.1" +source = { virtual = "." } diff --git a/2026/props/with_async.py b/2026/props/with_async.py new file mode 100644 index 00000000..25560b21 --- /dev/null +++ b/2026/props/with_async.py @@ -0,0 +1,97 @@ +import asyncio +from dataclasses import dataclass + +from user_repo import AccountStatus, InMemoryUserRepository, UserRepository + + +class BadUserAccount: + def __init__(self, user_id: int, repo: UserRepository) -> None: + self.user_id = user_id + self._repo = repo + + @property + async def username(self) -> str: + # Looks cheap, but does I/O every time + return await self._repo.fetch_username(self.user_id) + + @property + async def is_active(self) -> bool: + # Derived value that ALSO hides I/O + status = await self._repo.fetch_status(self.user_id) + return status is AccountStatus.ACTIVE + + +# ---- โœ… Better design: explicit async boundaries ---- + + +@dataclass +class UserAccount: + user_id: int + username: str + email: str + status: AccountStatus + + @classmethod + async def load(cls, user_id: int, repo: UserRepository) -> "UserAccount": + username, email, status = await asyncio.gather( + repo.fetch_username(user_id), + repo.fetch_email(user_id), + repo.fetch_status(user_id), + ) + return cls( + user_id=user_id, + username=username, + email=email, + status=status, + ) + + @property + def is_active(self) -> bool: + return self.status is AccountStatus.ACTIVE + + async def save(self, repo: UserRepository) -> None: + await asyncio.gather( + repo.update_username(self.user_id, self.username), + repo.update_email(self.user_id, self.email), + repo.update_status(self.user_id, self.status), + ) + + +# ---- Demo ---- + + +async def main() -> None: + repo = InMemoryUserRepository() + + print("=== Bad design: async properties ===") + bad = BadUserAccount(user_id=101, repo=repo) + print("username (1):", await bad.username) + print("username (2):", await bad.username, "(fetched twice)") + print("is_active:", await bad.is_active) + + print("\n=== Better design: explicit load + save ===") + account = await UserAccount.load(user_id=101, repo=repo) + print("Loaded:", account) + + account.email = "new-email@arjancodes.com" + await account.save(repo) + + reloaded = await UserAccount.load(user_id=101, repo=repo) + print("Reloaded:", reloaded) + + print("\n=== create/delete demo ===") + await repo.create_user( + 999, + username="jordan", + email="jordan@arjancodes.com", + status=AccountStatus.ACTIVE, + ) + created = await UserAccount.load(999, repo) + print("Created:", created) + + await repo.delete_user(999) + print("Deleted user 999") + + +if __name__ == "__main__": + asyncio.run(main()) From f1d800b9efeb5e93e7a8b5be76e917dfb3e613c2 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 13 Feb 2026 16:07:10 +0100 Subject: [PATCH 090/113] Added code example. --- 2026/state/final.py | 79 +++++++++++++++++++++++ 2026/state/sm.py | 42 +++++++++++++ 2026/state/traditional.py | 129 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 2026/state/final.py create mode 100644 2026/state/sm.py create mode 100644 2026/state/traditional.py diff --git a/2026/state/final.py b/2026/state/final.py new file mode 100644 index 00000000..27e377f5 --- /dev/null +++ b/2026/state/final.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass, field +from enum import Enum, auto + +from sm import StateMachine + + +class PayState(Enum): + NEW = auto() + AUTHORIZED = auto() + CAPTURED = auto() + FAILED = auto() + REFUNDED = auto() + + +class PayEvent(Enum): + AUTHORIZE = auto() + CAPTURE = auto() + FAIL = auto() + REFUND = auto() + + +@dataclass +class PaymentCtx: + payment_id: str + audit: list[str] = field(default_factory=list[str]) + + +# Create an instance: this is "the machine" +pay_sm: StateMachine[PayState, PayEvent, PaymentCtx] = StateMachine() + + +@pay_sm.transition(PayState.NEW, PayEvent.AUTHORIZE, PayState.AUTHORIZED) +def authorize(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: authorized") + + +@pay_sm.transition((PayState.NEW, PayState.AUTHORIZED), PayEvent.FAIL, PayState.FAILED) +def fail(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: failed") + + +@pay_sm.transition(PayState.AUTHORIZED, PayEvent.CAPTURE, PayState.CAPTURED) +def capture(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: captured") + + +@pay_sm.transition( + (PayState.AUTHORIZED, PayState.CAPTURED), PayEvent.REFUND, PayState.REFUNDED +) +def refund(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: refunded") + + +@dataclass +class Payment: + ctx: PaymentCtx + state: PayState = PayState.NEW + + def handle(self, event: PayEvent) -> None: + self.state = pay_sm.handle(self.ctx, self.state, event) + + +def main(): + p = Payment(ctx=PaymentCtx("p1")) + + p.handle(PayEvent.AUTHORIZE) + p.handle(PayEvent.CAPTURE) + p.handle(PayEvent.REFUND) + + print("state:", p.state) + print("audit:", p.ctx.audit) + + # Uncomment to see an invalid transition: + # p2 = Payment(ctx=PaymentCtx("p2", [])) + # p2.handle(PayEvent.CAPTURE) + + +if __name__ == "__main__": + main() diff --git a/2026/state/sm.py b/2026/state/sm.py new file mode 100644 index 00000000..c179a605 --- /dev/null +++ b/2026/state/sm.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Callable, Iterable + +type Action[C] = Callable[[C], None] +type Transition[S: Enum, C] = tuple[S, Action[C]] + + +class InvalidTransition(Exception): + pass + + +@dataclass +class StateMachine[S: Enum, E: Enum, C]: + transitions: dict[tuple[S, E], Transition[S, C]] = field( + default_factory=dict[tuple[S, E], Transition[S, C]] + ) + + def _register(self, from_state: S, event: E, to_state: S, func: Action[C]) -> None: + self.transitions[(from_state, event)] = (to_state, func) + + def _next_state(self, state: S, event: E) -> Transition[S, C]: + try: + return self.transitions[(state, event)] + except KeyError as e: + raise InvalidTransition(f"Cannot {event.name} when {state.name}") from e + + def transition(self, from_state: S | Iterable[S], event: E, to_state: S): + if not isinstance(from_state, Iterable): + from_state = (from_state,) + + def decorator(func: Action[C]) -> Action[C]: + for s in from_state: + self._register(s, event, to_state, func) + return func + + return decorator + + def handle(self, ctx: C, state: S, event: E) -> S: + next_state, action = self._next_state(state, event) + action(ctx) + return next_state diff --git a/2026/state/traditional.py b/2026/state/traditional.py new file mode 100644 index 00000000..3e342761 --- /dev/null +++ b/2026/state/traditional.py @@ -0,0 +1,129 @@ +from dataclasses import dataclass +from typing import Protocol + + +class PaymentState(Protocol): + """Common interface for all states.""" + + def authorize(self, payment: "Payment") -> None: ... + def capture(self, payment: "Payment") -> None: ... + def fail(self, payment: "Payment") -> None: ... + def refund(self, payment: "Payment") -> None: ... + + +@dataclass +class Payment: + """The Context: delegates behavior to the current state.""" + + payment_id: str + audit: list[str] + state: PaymentState + + def authorize(self) -> None: + self.state.authorize(self) + + def capture(self) -> None: + self.state.capture(self) + + def fail(self) -> None: + self.state.fail(self) + + def refund(self) -> None: + self.state.refund(self) + + +# ---------- Concrete States ---------- + + +class New: + def authorize(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: authorized") + payment.state = Authorized() + + def capture(self, payment: Payment) -> None: + raise RuntimeError("Cannot capture before authorize") + + def fail(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: failed") + payment.state = Failed() + + def refund(self, payment: Payment) -> None: + raise RuntimeError("Cannot refund a new payment") + + +class Authorized: + def authorize(self, payment: Payment) -> None: + raise RuntimeError("Already authorized") + + def capture(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: captured") + payment.state = Captured() + + def fail(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: failed") + payment.state = Failed() + + def refund(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: refunded") + payment.state = Refunded() + + +class Captured: + def authorize(self, payment: Payment) -> None: + raise RuntimeError("Already captured") + + def capture(self, payment: Payment) -> None: + raise RuntimeError("Already captured") + + def fail(self, payment: Payment) -> None: + raise RuntimeError("Cannot fail after capture") + + def refund(self, payment: Payment) -> None: + payment.audit.append(f"{payment.payment_id}: refunded") + payment.state = Refunded() + + +class Failed: + def authorize(self, payment: Payment) -> None: + raise RuntimeError("Cannot authorize a failed payment") + + def capture(self, payment: Payment) -> None: + raise RuntimeError("Cannot capture a failed payment") + + def fail(self, payment: Payment) -> None: + raise RuntimeError("Already failed") + + def refund(self, payment: Payment) -> None: + raise RuntimeError("Nothing to refund") + + +class Refunded: + def authorize(self, payment: Payment) -> None: + raise RuntimeError("Refunded payments stay refunded") + + def capture(self, payment: Payment) -> None: + raise RuntimeError("Refunded payments stay refunded") + + def fail(self, payment: Payment) -> None: + raise RuntimeError("Refunded payments stay refunded") + + def refund(self, payment: Payment) -> None: + raise RuntimeError("Already refunded") + + +# ---------- Demo ---------- + + +def main() -> None: + p = Payment(payment_id="p1", audit=[], state=New()) + + p.authorize() + p.capture() + p.refund() + + print("final state:", type(p.state).__name__) + print("audit:", p.audit) + + +if __name__ == "__main__": + main() From 6ab78a8b23ba61acc96fd26a32ff0bec9668a403 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 13 Feb 2026 16:07:37 +0100 Subject: [PATCH 091/113] Added pyproject file. --- 2026/state/pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 2026/state/pyproject.toml diff --git a/2026/state/pyproject.toml b/2026/state/pyproject.toml new file mode 100644 index 00000000..6f38f510 --- /dev/null +++ b/2026/state/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "state" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] From 0d8e2bca7d95d16af92618f5be695b844f7f8437 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 24 Feb 2026 08:55:39 +0100 Subject: [PATCH 092/113] WiP on state example. --- 2026/state/manual.py | 117 +++++++++++++++++++++++++++++++++++++++++++ 2026/state/sm.py | 18 ++++--- 2 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 2026/state/manual.py diff --git a/2026/state/manual.py b/2026/state/manual.py new file mode 100644 index 00000000..b87fc3f2 --- /dev/null +++ b/2026/state/manual.py @@ -0,0 +1,117 @@ +from dataclasses import dataclass, field +from enum import Enum, auto + +from sm import StateMachine + + +class PayState(Enum): + NEW = auto() + AUTHORIZED = auto() + CAPTURED = auto() + FAILED = auto() + REFUNDED = auto() + + +class PayEvent(Enum): + AUTHORIZE = auto() + CAPTURE = auto() + FAIL = auto() + REFUND = auto() + + +@dataclass +class PaymentCtx: + payment_id: str + audit: list[str] = field(default_factory=list[str]) + + +# Create the machine +pay_sm: StateMachine[PayState, PayEvent, PaymentCtx] = StateMachine() + + +# ----- Define transition actions ----- + + +def authorize(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: authorized") + + +def capture(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: captured") + + +def fail(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: failed") + + +def refund(ctx: PaymentCtx) -> None: + ctx.audit.append(f"{ctx.payment_id}: refunded") + + +# ----- Register transitions explicitly ----- + +pay_sm.add_transition( + PayState.NEW, + PayEvent.AUTHORIZE, + PayState.AUTHORIZED, + authorize, +) + +pay_sm.add_transition( + PayState.NEW, + PayEvent.FAIL, + PayState.FAILED, + fail, +) + +pay_sm.add_transition( + PayState.AUTHORIZED, + PayEvent.CAPTURE, + PayState.CAPTURED, + capture, +) + +pay_sm.add_transition( + PayState.AUTHORIZED, + PayEvent.FAIL, + PayState.FAILED, + fail, +) + +pay_sm.add_transition( + PayState.AUTHORIZED, + PayEvent.REFUND, + PayState.REFUNDED, + refund, +) + +pay_sm.add_transition( + PayState.CAPTURED, + PayEvent.REFUND, + PayState.REFUNDED, + refund, +) + + +@dataclass +class Payment: + ctx: PaymentCtx + state: PayState = PayState.NEW + + def handle(self, event: PayEvent) -> None: + self.state = pay_sm.handle(self.ctx, self.state, event) + + +def main(): + p = Payment(ctx=PaymentCtx("p1")) + + p.handle(PayEvent.AUTHORIZE) + p.handle(PayEvent.CAPTURE) + p.handle(PayEvent.REFUND) + + print("state:", p.state) + print("audit:", p.ctx.audit) + + +if __name__ == "__main__": + main() diff --git a/2026/state/sm.py b/2026/state/sm.py index c179a605..331d7725 100644 --- a/2026/state/sm.py +++ b/2026/state/sm.py @@ -16,27 +16,29 @@ class StateMachine[S: Enum, E: Enum, C]: default_factory=dict[tuple[S, E], Transition[S, C]] ) - def _register(self, from_state: S, event: E, to_state: S, func: Action[C]) -> None: + def add_transition( + self, from_state: S, event: E, to_state: S, func: Action[C] + ) -> None: self.transitions[(from_state, event)] = (to_state, func) - def _next_state(self, state: S, event: E) -> Transition[S, C]: + def next_transition(self, state: S, event: E) -> Transition[S, C]: try: return self.transitions[(state, event)] except KeyError as e: raise InvalidTransition(f"Cannot {event.name} when {state.name}") from e + def handle(self, ctx: C, state: S, event: E) -> S: + next_state, action = self.next_transition(state, event) + action(ctx) + return next_state + def transition(self, from_state: S | Iterable[S], event: E, to_state: S): if not isinstance(from_state, Iterable): from_state = (from_state,) def decorator(func: Action[C]) -> Action[C]: for s in from_state: - self._register(s, event, to_state, func) + self.add_transition(s, event, to_state, func) return func return decorator - - def handle(self, ctx: C, state: S, event: E) -> S: - next_state, action = self._next_state(state, event) - action(ctx) - return next_state From e1b3c820c36feafafe03ebf63bafa370a116f707 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 26 Feb 2026 13:43:25 +0100 Subject: [PATCH 093/113] Simplified state machine types. --- 2026/state/sm.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/2026/state/sm.py b/2026/state/sm.py index 331d7725..43f35556 100644 --- a/2026/state/sm.py +++ b/2026/state/sm.py @@ -3,7 +3,6 @@ from typing import Callable, Iterable type Action[C] = Callable[[C], None] -type Transition[S: Enum, C] = tuple[S, Action[C]] class InvalidTransition(Exception): @@ -12,8 +11,8 @@ class InvalidTransition(Exception): @dataclass class StateMachine[S: Enum, E: Enum, C]: - transitions: dict[tuple[S, E], Transition[S, C]] = field( - default_factory=dict[tuple[S, E], Transition[S, C]] + transitions: dict[tuple[S, E], tuple[S, Action[C]]] = field( + default_factory=dict[tuple[S, E], tuple[S, Action[C]]] ) def add_transition( @@ -21,7 +20,7 @@ def add_transition( ) -> None: self.transitions[(from_state, event)] = (to_state, func) - def next_transition(self, state: S, event: E) -> Transition[S, C]: + def next_transition(self, state: S, event: E) -> tuple[S, Action[C]]: try: return self.transitions[(state, event)] except KeyError as e: From a21d051e483f62188c3b87da7fbc657c84f2da29 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 13 Feb 2026 14:03:59 +0100 Subject: [PATCH 094/113] Added code examples. --- 2026/descriptors/lazy_prop.py | 47 +++++++++++++++++++++ 2026/descriptors/minimal.py | 24 +++++++++++ 2026/descriptors/non_data.py | 51 +++++++++++++++++++++++ 2026/descriptors/pyproject.toml | 6 +++ 2026/descriptors/tiny_prop.py | 33 +++++++++++++++ 2026/descriptors/validated.py | 72 +++++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+) create mode 100644 2026/descriptors/lazy_prop.py create mode 100644 2026/descriptors/minimal.py create mode 100644 2026/descriptors/non_data.py create mode 100644 2026/descriptors/pyproject.toml create mode 100644 2026/descriptors/tiny_prop.py create mode 100644 2026/descriptors/validated.py diff --git a/2026/descriptors/lazy_prop.py b/2026/descriptors/lazy_prop.py new file mode 100644 index 00000000..2e8a5b96 --- /dev/null +++ b/2026/descriptors/lazy_prop.py @@ -0,0 +1,47 @@ +from typing import Any, Callable, Self + + +class lazy_property[T]: + def __init__(self, func: Callable[[Any], T]) -> None: + self.func = func + self.name = func.__name__ + self.storage_name = f"_{self.name}" + + def __get__(self, instance: Any | None, owner: type) -> T | Self: + if instance is None: + return self + if hasattr(instance, self.storage_name): + return getattr(instance, self.storage_name) + value = self.func(instance) + setattr(instance, self.storage_name, value) + return value + + +class Report: + def __init__(self, rows: list[dict[str, Any]]) -> None: + self.rows = rows + + @lazy_property + def revenue_by_country(self) -> dict[str, float]: + print("computing revenue_by_country...") + result: dict[str, float] = {} + for r in self.rows: + country = str(r["country"]) + revenue = float(r["revenue"]) + result[country] = result.get(country, 0.0) + revenue + return result + + +def main() -> None: + rows: list[dict[str, Any]] = [ + {"country": "NL", "revenue": 10}, + {"country": "NL", "revenue": 5}, + ] + + rep = Report(rows) + print(rep.revenue_by_country) + print(rep.revenue_by_country) # cached + + +if __name__ == "__main__": + main() diff --git a/2026/descriptors/minimal.py b/2026/descriptors/minimal.py new file mode 100644 index 00000000..041bcb18 --- /dev/null +++ b/2026/descriptors/minimal.py @@ -0,0 +1,24 @@ +class DemoDescriptor: + def __get__(self, instance: object | None, owner: type) -> int: + print(f"__get__ called with instance={instance}, owner={owner.__name__}") + return 42 + + def __set__(self, instance: object, value: int) -> None: + print(f"__set__ called with instance={instance}, value={value}") + + +class Thing: + x = DemoDescriptor() + + +def main() -> None: + + t = Thing() + + print(t.x) # triggers __get__ + t.x = 10 # triggers __set__ + print(Thing.x) # instance=None + + +if __name__ == "__main__": + main() diff --git a/2026/descriptors/non_data.py b/2026/descriptors/non_data.py new file mode 100644 index 00000000..f714dc4c --- /dev/null +++ b/2026/descriptors/non_data.py @@ -0,0 +1,51 @@ +from typing import Self + +# ============================================================ +# 3) Non-data descriptor (only __get__) can be shadowed +# ============================================================ + + +class NonData: + def __get__(self, instance: object | None, owner: type) -> str | Self: + if instance is None: + return self + return "from descriptor" + + +class A: + x: NonData = NonData() + + +# ============================================================ +# 4) Data descriptor (has __set__) cannot be shadowed +# ============================================================ + + +class Data: + def __get__(self, instance: object | None, owner: type) -> str | Self: + if instance is None: + return self + return "from descriptor" + + def __set__(self, instance: object, value: str) -> None: + instance.__dict__["x"] = value + + +class B: + x: Data = Data() + + +def main() -> None: + + a = A() + print(a.x) + a.__dict__["x"] = "from instance dict" + print(a.x) # shadowed by instance dict + + b = B() + b.__dict__["x"] = "from instance dict" + print(b.x) # descriptor still wins (data descriptor precedence) + + +if __name__ == "__main__": + main() diff --git a/2026/descriptors/pyproject.toml b/2026/descriptors/pyproject.toml new file mode 100644 index 00000000..97c8c470 --- /dev/null +++ b/2026/descriptors/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "descriptors" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/descriptors/tiny_prop.py b/2026/descriptors/tiny_prop.py new file mode 100644 index 00000000..753bdebf --- /dev/null +++ b/2026/descriptors/tiny_prop.py @@ -0,0 +1,33 @@ +from typing import Any, Callable + + +class SimpleProperty: + def __init__(self, fget: Callable[[Any], Any]) -> None: + self.fget = fget + + def __get__(self, instance: Any | None, owner: type) -> Any: + if instance is None: + return self + return self.fget(instance) + + +class User: + def __init__(self, first: str, last: str) -> None: + self.first = first + self.last = last + + @SimpleProperty + def full_name(self) -> str: + return f"{self.first} {self.last}" + + +def main() -> None: + + u = User("Arjan", "Egges") + + print(u.full_name) + print(User.full_name) + + +if __name__ == "__main__": + main() diff --git a/2026/descriptors/validated.py b/2026/descriptors/validated.py new file mode 100644 index 00000000..b12eb13b --- /dev/null +++ b/2026/descriptors/validated.py @@ -0,0 +1,72 @@ +from typing import Any, Callable, Self + +Validator = Callable[[str, Any], None] + + +def non_empty(field: str, value: Any) -> None: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string") + + +def min_value(n: int) -> Validator: + def _v(field: str, value: Any) -> None: + if value < n: + raise ValueError(f"{field} must be >= {n}") + + return _v + + +class ValidatedField[T]: + def __init__( + self, + cast: Callable[[Any], T], + validators: tuple[Validator, ...] = (), + ) -> None: + self.cast = cast + self.validators = validators + self.name: str = "" + self.storage_name: str = "" + + def __set_name__(self, owner: type, name: str) -> None: + self.name = name + self.storage_name = f"_{name}" + + def __get__(self, instance: object | None, owner: type) -> T | Self | None: + if instance is None: + return self + return getattr(instance, self.storage_name, None) + + def __set__(self, instance: object, value: Any) -> None: + casted = self.cast(value) + for v in self.validators: + v(self.name, casted) + setattr(instance, self.storage_name, casted) + + +class Customer: + name: ValidatedField[str] = ValidatedField(cast=str, validators=(non_empty,)) + age: ValidatedField[int] = ValidatedField(cast=int, validators=(min_value(18),)) + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + +def main() -> None: + + c = Customer("Arjan", 48) + print(c.name, c.age) + + try: + c.name = " " + except ValueError as e: + print(e) + + try: + c.age = 17 + except ValueError as e: + print(e) + + +if __name__ == "__main__": + main() From 664df04f77b37edd3b19271dbff8c1a11e2ccac1 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 12 Feb 2026 17:15:43 +0100 Subject: [PATCH 095/113] Work in progress on code example. --- 2026/clean/before.py | 232 ++++++++++++++++++++++++++++++++++++++ 2026/clean/pyproject.toml | 6 + 2 files changed, 238 insertions(+) create mode 100644 2026/clean/before.py create mode 100644 2026/clean/pyproject.toml diff --git a/2026/clean/before.py b/2026/clean/before.py new file mode 100644 index 00000000..4fd77953 --- /dev/null +++ b/2026/clean/before.py @@ -0,0 +1,232 @@ +import csv +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, Protocol + +# ---------- Domain-ish types ---------- + + +@dataclass(frozen=True) +class Row: + data: dict[str, Any] + + +@dataclass(frozen=True) +class SalesSummary: + total_orders: int + total_revenue: float + + +# ---------- Abstraction (nice!) ---------- + + +class ReportService(Protocol): + """Clean interface. The chaos is hidden in the implementation + wiring.""" + + def run(self, source: str, target: str) -> None: ... + + +# ---------- Implementation (not nice) ---------- + + +class DefaultReportService: + """ + The call site looks clean: service.run(source, target) + + But the actual "API" is this constructor: a bag of settings and flags. + """ + + def __init__( + self, + *, + delimiter: str, + encoding: str, + expected_fields: tuple[str, ...], + allow_negative_revenue: bool, + drop_invalid_rows: bool, + country: str | None, + min_revenue: float, + revenue_field: str, + order_id_field: str, + report_title: str, + currency_symbol: str, + include_debug_footer: bool, + row_transform: Callable[[Row], Row] | None, + on_error: Callable[[Exception, Row | None], None] | None, + ) -> None: + self._delimiter = delimiter + self._encoding = encoding + self._expected_fields = expected_fields + self._allow_negative_revenue = allow_negative_revenue + self._drop_invalid_rows = drop_invalid_rows + self._country = country + self._min_revenue = min_revenue + self._revenue_field = revenue_field + self._order_id_field = order_id_field + self._report_title = report_title + self._currency_symbol = currency_symbol + self._include_debug_footer = include_debug_footer + self._row_transform = row_transform + self._on_error = on_error + + def run(self, source: str, target: str) -> None: + path = Path(source) + + # --- Load + parse CSV (for real this time) --- + if not path.exists(): + raise FileNotFoundError(f"CSV file not found: {path.resolve()}") + + raw_rows: list[Row] = [] + with path.open("r", newline="", encoding=self._encoding) as f: + reader = csv.DictReader(f, delimiter=self._delimiter) + # DictReader already uses the header row as keys. + for d in reader: + # Keep raw strings; validation will decide what to do. + raw_rows.append(Row(data=dict(d))) + + # --- Validate + Transform + Filter --- + processed: list[Row] = [] + for row in raw_rows: + try: + # Required fields must exist AND be non-empty + missing_or_empty = [ + f for f in self._expected_fields if not row.data.get(f) + ] + if missing_or_empty: + raise ValueError(f"Missing/empty fields: {missing_or_empty}") + + # Coerce revenue + revenue_raw = row.data[self._revenue_field] + revenue = float(revenue_raw) + + if revenue < 0 and not self._allow_negative_revenue: + raise ValueError("Negative revenue is not allowed") + + # Optional transform hook + if self._row_transform is not None: + row = self._row_transform(row) + + # Filters + if ( + self._country is not None + and row.data.get("country") != self._country + ): + continue + if revenue < self._min_revenue: + continue + + processed.append(row) + + except Exception as exc: + if self._on_error is not None: + self._on_error(exc, row) + + if self._drop_invalid_rows: + continue + raise + + # --- Aggregate --- + total_orders = 0 + total_revenue = 0.0 + for row in processed: + total_orders += 1 + total_revenue += float(row.data[self._revenue_field]) + + summary = SalesSummary(total_orders=total_orders, total_revenue=total_revenue) + + # --- Export --- + # target is still mostly decorative, but we pretend it's a real output concern. + if target == "stdout": + print(self._report_title) + print(f"Orders: {summary.total_orders}") + print(f"Revenue: {self._currency_symbol}{summary.total_revenue:.2f}") + else: + out = Path(target) + out.write_text( + "\n".join( + [ + self._report_title, + f"Orders: {summary.total_orders}", + f"Revenue: {self._currency_symbol}{summary.total_revenue:.2f}", + "", + ] + ), + encoding="utf-8", + ) + print(f"Wrote report to {out.resolve()}") + + if self._include_debug_footer: + print("--- DEBUG ---") + print( + f"source={source!r} target={target!r} " + f"delimiter={self._delimiter!r} encoding={self._encoding!r}" + ) + print( + f"filters: country={self._country!r}, min_revenue={self._min_revenue}" + ) + print( + f"fields: revenue_field={self._revenue_field!r}, order_id_field={self._order_id_field!r}" + ) + print(f"loaded_rows={len(raw_rows)} processed_rows={len(processed)}") + + +# ---------- "DI container" wiring ---------- + + +class Container: + def __init__(self) -> None: + self._settings = self._load_settings() + + def _load_settings(self) -> dict[str, Any]: + # Pretend this comes from env vars, YAML, or a config framework. + return { + "delimiter": ",", + "encoding": "utf-8", + "expected_fields": ("order_id", "country", "revenue"), + "allow_negative_revenue": False, + "drop_invalid_rows": True, + "country": "NL", + "min_revenue": 10.0, + "revenue_field": "revenue", + "order_id_field": "order_id", + "report_title": "Sales Report (NL, revenue >= 10)", + "currency_symbol": "โ‚ฌ", + "include_debug_footer": True, + } + + def report_service(self) -> ReportService: + def on_error(exc: Exception, row: Row | None) -> None: + # Cross-cutting concern living in the container, naturally. + # Also: the best place to hide production bugs. + payload = row.data if row else None + print(f"[warn] {exc} row={payload}") + + return DefaultReportService( + delimiter=str(self._settings["delimiter"]), + encoding=str(self._settings["encoding"]), + expected_fields=tuple(self._settings["expected_fields"]), + allow_negative_revenue=bool(self._settings["allow_negative_revenue"]), + drop_invalid_rows=bool(self._settings["drop_invalid_rows"]), + country=self._settings["country"], + min_revenue=float(self._settings["min_revenue"]), + revenue_field=str(self._settings["revenue_field"]), + order_id_field=str(self._settings["order_id_field"]), + report_title=str(self._settings["report_title"]), + currency_symbol=str(self._settings["currency_symbol"]), + include_debug_footer=bool(self._settings["include_debug_footer"]), + row_transform=None, + on_error=on_error, + ) + + +# ---------- Call site (super clean!) ---------- + + +def main() -> None: + container = Container() + service = container.report_service() + service.run(source="sales.csv", target="stdout") + + +if __name__ == "__main__": + main() diff --git a/2026/clean/pyproject.toml b/2026/clean/pyproject.toml new file mode 100644 index 00000000..57789429 --- /dev/null +++ b/2026/clean/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "clean" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ +] From 41c6d9b64b6afe2610b550d9a188885f54043a07 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Fri, 13 Feb 2026 11:48:16 +0100 Subject: [PATCH 096/113] Reworked example. Added sample sales csv. --- 2026/clean/before.py | 232 ------------------------------- 2026/clean/generate_sales.csv.py | 33 +++++ 2026/clean/report_after.py | 86 ++++++++++++ 2026/clean/report_before.py | 122 ++++++++++++++++ 2026/clean/sales.csv | 53 +++++++ 2026/clean/uv.lock | 8 ++ 6 files changed, 302 insertions(+), 232 deletions(-) delete mode 100644 2026/clean/before.py create mode 100644 2026/clean/generate_sales.csv.py create mode 100644 2026/clean/report_after.py create mode 100644 2026/clean/report_before.py create mode 100644 2026/clean/sales.csv create mode 100644 2026/clean/uv.lock diff --git a/2026/clean/before.py b/2026/clean/before.py deleted file mode 100644 index 4fd77953..00000000 --- a/2026/clean/before.py +++ /dev/null @@ -1,232 +0,0 @@ -import csv -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable, Protocol - -# ---------- Domain-ish types ---------- - - -@dataclass(frozen=True) -class Row: - data: dict[str, Any] - - -@dataclass(frozen=True) -class SalesSummary: - total_orders: int - total_revenue: float - - -# ---------- Abstraction (nice!) ---------- - - -class ReportService(Protocol): - """Clean interface. The chaos is hidden in the implementation + wiring.""" - - def run(self, source: str, target: str) -> None: ... - - -# ---------- Implementation (not nice) ---------- - - -class DefaultReportService: - """ - The call site looks clean: service.run(source, target) - - But the actual "API" is this constructor: a bag of settings and flags. - """ - - def __init__( - self, - *, - delimiter: str, - encoding: str, - expected_fields: tuple[str, ...], - allow_negative_revenue: bool, - drop_invalid_rows: bool, - country: str | None, - min_revenue: float, - revenue_field: str, - order_id_field: str, - report_title: str, - currency_symbol: str, - include_debug_footer: bool, - row_transform: Callable[[Row], Row] | None, - on_error: Callable[[Exception, Row | None], None] | None, - ) -> None: - self._delimiter = delimiter - self._encoding = encoding - self._expected_fields = expected_fields - self._allow_negative_revenue = allow_negative_revenue - self._drop_invalid_rows = drop_invalid_rows - self._country = country - self._min_revenue = min_revenue - self._revenue_field = revenue_field - self._order_id_field = order_id_field - self._report_title = report_title - self._currency_symbol = currency_symbol - self._include_debug_footer = include_debug_footer - self._row_transform = row_transform - self._on_error = on_error - - def run(self, source: str, target: str) -> None: - path = Path(source) - - # --- Load + parse CSV (for real this time) --- - if not path.exists(): - raise FileNotFoundError(f"CSV file not found: {path.resolve()}") - - raw_rows: list[Row] = [] - with path.open("r", newline="", encoding=self._encoding) as f: - reader = csv.DictReader(f, delimiter=self._delimiter) - # DictReader already uses the header row as keys. - for d in reader: - # Keep raw strings; validation will decide what to do. - raw_rows.append(Row(data=dict(d))) - - # --- Validate + Transform + Filter --- - processed: list[Row] = [] - for row in raw_rows: - try: - # Required fields must exist AND be non-empty - missing_or_empty = [ - f for f in self._expected_fields if not row.data.get(f) - ] - if missing_or_empty: - raise ValueError(f"Missing/empty fields: {missing_or_empty}") - - # Coerce revenue - revenue_raw = row.data[self._revenue_field] - revenue = float(revenue_raw) - - if revenue < 0 and not self._allow_negative_revenue: - raise ValueError("Negative revenue is not allowed") - - # Optional transform hook - if self._row_transform is not None: - row = self._row_transform(row) - - # Filters - if ( - self._country is not None - and row.data.get("country") != self._country - ): - continue - if revenue < self._min_revenue: - continue - - processed.append(row) - - except Exception as exc: - if self._on_error is not None: - self._on_error(exc, row) - - if self._drop_invalid_rows: - continue - raise - - # --- Aggregate --- - total_orders = 0 - total_revenue = 0.0 - for row in processed: - total_orders += 1 - total_revenue += float(row.data[self._revenue_field]) - - summary = SalesSummary(total_orders=total_orders, total_revenue=total_revenue) - - # --- Export --- - # target is still mostly decorative, but we pretend it's a real output concern. - if target == "stdout": - print(self._report_title) - print(f"Orders: {summary.total_orders}") - print(f"Revenue: {self._currency_symbol}{summary.total_revenue:.2f}") - else: - out = Path(target) - out.write_text( - "\n".join( - [ - self._report_title, - f"Orders: {summary.total_orders}", - f"Revenue: {self._currency_symbol}{summary.total_revenue:.2f}", - "", - ] - ), - encoding="utf-8", - ) - print(f"Wrote report to {out.resolve()}") - - if self._include_debug_footer: - print("--- DEBUG ---") - print( - f"source={source!r} target={target!r} " - f"delimiter={self._delimiter!r} encoding={self._encoding!r}" - ) - print( - f"filters: country={self._country!r}, min_revenue={self._min_revenue}" - ) - print( - f"fields: revenue_field={self._revenue_field!r}, order_id_field={self._order_id_field!r}" - ) - print(f"loaded_rows={len(raw_rows)} processed_rows={len(processed)}") - - -# ---------- "DI container" wiring ---------- - - -class Container: - def __init__(self) -> None: - self._settings = self._load_settings() - - def _load_settings(self) -> dict[str, Any]: - # Pretend this comes from env vars, YAML, or a config framework. - return { - "delimiter": ",", - "encoding": "utf-8", - "expected_fields": ("order_id", "country", "revenue"), - "allow_negative_revenue": False, - "drop_invalid_rows": True, - "country": "NL", - "min_revenue": 10.0, - "revenue_field": "revenue", - "order_id_field": "order_id", - "report_title": "Sales Report (NL, revenue >= 10)", - "currency_symbol": "โ‚ฌ", - "include_debug_footer": True, - } - - def report_service(self) -> ReportService: - def on_error(exc: Exception, row: Row | None) -> None: - # Cross-cutting concern living in the container, naturally. - # Also: the best place to hide production bugs. - payload = row.data if row else None - print(f"[warn] {exc} row={payload}") - - return DefaultReportService( - delimiter=str(self._settings["delimiter"]), - encoding=str(self._settings["encoding"]), - expected_fields=tuple(self._settings["expected_fields"]), - allow_negative_revenue=bool(self._settings["allow_negative_revenue"]), - drop_invalid_rows=bool(self._settings["drop_invalid_rows"]), - country=self._settings["country"], - min_revenue=float(self._settings["min_revenue"]), - revenue_field=str(self._settings["revenue_field"]), - order_id_field=str(self._settings["order_id_field"]), - report_title=str(self._settings["report_title"]), - currency_symbol=str(self._settings["currency_symbol"]), - include_debug_footer=bool(self._settings["include_debug_footer"]), - row_transform=None, - on_error=on_error, - ) - - -# ---------- Call site (super clean!) ---------- - - -def main() -> None: - container = Container() - service = container.report_service() - service.run(source="sales.csv", target="stdout") - - -if __name__ == "__main__": - main() diff --git a/2026/clean/generate_sales.csv.py b/2026/clean/generate_sales.csv.py new file mode 100644 index 00000000..cd34b23a --- /dev/null +++ b/2026/clean/generate_sales.csv.py @@ -0,0 +1,33 @@ +import csv +import random +from pathlib import Path + + +def main() -> None: + random.seed(42) + path = Path("sales.csv") + + countries = ["NL", "BE", "DE"] + rows = [] + + for i in range(1, 51): + country = "NL" if random.random() < 0.45 else random.choice(countries) + revenue = round(max(0.0, random.gauss(50, 20)), 2) + rows.append( + {"order_id": f"{i:04d}", "country": country, "revenue": f"{revenue:.2f}"} + ) + + # a couple of "bad" rows to show validation noise + rows.append({"order_id": "BAD1", "country": "NL", "revenue": ""}) + rows.append({"order_id": "BAD2", "country": "NL", "revenue": "-10.00"}) + + with path.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=["order_id", "country", "revenue"]) + writer.writeheader() + writer.writerows(rows) + + print(f"Wrote {len(rows)} rows to {path}") + + +if __name__ == "__main__": + main() diff --git a/2026/clean/report_after.py b/2026/clean/report_after.py new file mode 100644 index 00000000..7e21a45f --- /dev/null +++ b/2026/clean/report_after.py @@ -0,0 +1,86 @@ +import csv +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class ReportConfig: + country: str = "NL" + min_revenue: float = 10.0 + allow_negative: bool = False + delimiter: str = "," + encoding: str = "utf-8" + + +@dataclass(frozen=True) +class Summary: + country: str + count: int + revenue_sum: float + + def to_text(self) -> str: + return ( + f"Country={self.country}, " + f"Count={self.count}, " + f"Revenue={self.revenue_sum:.2f}" + ) + + def export_stdout(self) -> None: + print(self.to_text()) + + def export_file(self, path: Path) -> None: + path.write_text(self.to_text() + "\n") + print(f"Wrote report to {path.resolve()}") + + +def load_data(source: Path, config: ReportConfig) -> list[dict[str, str]]: + if not source.exists(): + raise FileNotFoundError(source) + + with source.open("r", newline="", encoding=config.encoding) as f: + reader = csv.DictReader(f, delimiter=config.delimiter) + return [dict(r) for r in reader] + + +def summarize(rows: list[dict[str, str]], config: ReportConfig) -> Summary: + + def is_valid(r: dict[str, str]) -> bool: + if not r.get("country") or not r.get("revenue"): + return False + + revenue = float(r["revenue"]) + + if revenue < 0 and not config.allow_negative: + return False + + if r["country"] != config.country: + return False + + if revenue < config.min_revenue: + return False + + return True + + valid_rows = [r for r in rows if is_valid(r)] + + return Summary( + country=config.country, + count=len(valid_rows), + revenue_sum=sum(float(r["revenue"]) for r in valid_rows), + ) + + +def main() -> None: + config = ReportConfig(country="NL", min_revenue=10.0) + rows = load_data(Path("sales.csv"), config) + summary = summarize(rows, config) + + # Option 1: print to console + summary.export_stdout() + + # Option 2: write to file + summary.export_file(Path("report.txt")) + + +if __name__ == "__main__": + main() diff --git a/2026/clean/report_before.py b/2026/clean/report_before.py new file mode 100644 index 00000000..aaa0c025 --- /dev/null +++ b/2026/clean/report_before.py @@ -0,0 +1,122 @@ +import csv +from dataclasses import dataclass +from pathlib import Path +from typing import Protocol + + +@dataclass(frozen=True) +class Summary: + count: int + revenue_sum: float + + +# ---------- Clean-looking abstraction ---------- + + +class ReportService(Protocol): + def run(self, source: str, target: str) -> None: ... + + +# ---------- Implementation with orthogonal complexity ---------- + + +class DefaultReportService: + def __init__( + self, + *, + delimiter: str, + encoding: str, + country: str, + min_revenue: float, + allow_negative: bool, + ) -> None: + self._delimiter = delimiter + self._encoding = encoding + self._country = country + self._min_revenue = min_revenue + self._allow_negative = allow_negative + + def run(self, source: str, target: str) -> None: + path = Path(source) + if not path.exists(): + raise FileNotFoundError(path) + + # --- Load --- + with path.open("r", newline="", encoding=self._encoding) as f: + reader = csv.DictReader(f, delimiter=self._delimiter) + rows = [dict(r) for r in reader] + + # --- Validate + Filter + Aggregate --- + count = 0 + revenue_sum = 0.0 + + for r in rows: + country = r.get("country") + revenue_raw = r.get("revenue") + + if not country or not revenue_raw: + continue + + revenue = float(revenue_raw) + + if revenue < 0 and not self._allow_negative: + continue + + if country != self._country: + continue + + if revenue < self._min_revenue: + continue + + count += 1 + revenue_sum += revenue + + summary = Summary(count=count, revenue_sum=revenue_sum) + + # --- Export (two output options) --- + text = ( + f"Country={self._country}, " + f"Count={summary.count}, " + f"Revenue={summary.revenue_sum:.2f}" + ) + + if target == "stdout": + print(text) + else: + output_path = Path(target) + output_path.write_text(text + "\n", encoding=self._encoding) + print(f"Wrote report to {output_path.resolve()}") + + +# ---------- "DI container" wiring ---------- + + +class Container: + def __init__(self) -> None: + self._settings = { + "delimiter": ",", + "encoding": "utf-8", + "country": "NL", + "min_revenue": 10.0, + "allow_negative": False, + } + + def report_service(self) -> ReportService: + return DefaultReportService(**self._settings) + + +# ---------- Clean call site ---------- + + +def main() -> None: + service = Container().report_service() + + # Option 1: stdout + service.run("sales.csv", "stdout") + + # Option 2: file output + service.run("sales.csv", "report.txt") + + +if __name__ == "__main__": + main() diff --git a/2026/clean/sales.csv b/2026/clean/sales.csv new file mode 100644 index 00000000..fed17422 --- /dev/null +++ b/2026/clean/sales.csv @@ -0,0 +1,53 @@ +order_id,country,revenue +0001,NL,49.20 +0002,NL,35.03 +0003,NL,48.53 +0004,NL,24.93 +0005,NL,54.65 +0006,NL,73.27 +0007,NL,35.23 +0008,NL,29.71 +0009,NL,50.65 +0010,NL,38.22 +0011,NL,57.34 +0012,NL,83.16 +0013,NL,38.39 +0014,NL,64.22 +0015,NL,40.01 +0016,NL,52.60 +0017,DE,20.75 +0018,DE,23.04 +0019,NL,31.77 +0020,NL,20.77 +0021,BE,45.03 +0022,NL,77.98 +0023,NL,53.97 +0024,DE,65.26 +0025,NL,33.65 +0026,NL,31.46 +0027,BE,78.53 +0028,DE,48.12 +0029,NL,50.78 +0030,BE,43.26 +0031,NL,90.44 +0032,DE,67.86 +0033,NL,38.76 +0034,BE,33.37 +0035,NL,71.13 +0036,DE,75.43 +0037,DE,38.65 +0038,DE,58.32 +0039,NL,57.40 +0040,NL,41.18 +0041,DE,68.07 +0042,DE,57.62 +0043,NL,4.86 +0044,NL,34.31 +0045,DE,50.19 +0046,NL,15.78 +0047,NL,50.88 +0048,DE,51.35 +0049,NL,37.45 +0050,DE,49.95 +BAD1,NL, +BAD2,NL,-10.00 diff --git a/2026/clean/uv.lock b/2026/clean/uv.lock new file mode 100644 index 00000000..05e56777 --- /dev/null +++ b/2026/clean/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "clean" +version = "0.1.0" +source = { virtual = "." } From 6794ec72b301ee4fab485832c99bd19874cbd7ac Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 26 Feb 2026 10:51:31 +0100 Subject: [PATCH 097/113] Cleaned up after version. --- 2026/clean/report_after.py | 42 ++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/2026/clean/report_after.py b/2026/clean/report_after.py index 7e21a45f..7dfefa21 100644 --- a/2026/clean/report_after.py +++ b/2026/clean/report_after.py @@ -1,7 +1,11 @@ import csv +import json from dataclasses import dataclass from pathlib import Path +type Data = dict[str, str] +type JSON = dict[str, str | int | float] + @dataclass(frozen=True) class ReportConfig: @@ -25,15 +29,15 @@ def to_text(self) -> str: f"Revenue={self.revenue_sum:.2f}" ) - def export_stdout(self) -> None: - print(self.to_text()) - - def export_file(self, path: Path) -> None: - path.write_text(self.to_text() + "\n") - print(f"Wrote report to {path.resolve()}") + def to_json(self) -> JSON: + return { + "country": self.country, + "count": self.count, + "revenue_sum": self.revenue_sum, + } -def load_data(source: Path, config: ReportConfig) -> list[dict[str, str]]: +def load_data(source: Path, config: ReportConfig) -> list[Data]: if not source.exists(): raise FileNotFoundError(source) @@ -42,9 +46,9 @@ def load_data(source: Path, config: ReportConfig) -> list[dict[str, str]]: return [dict(r) for r in reader] -def summarize(rows: list[dict[str, str]], config: ReportConfig) -> Summary: +def summarize(rows: list[Data], config: ReportConfig) -> Summary: - def is_valid(r: dict[str, str]) -> bool: + def is_valid(r: Data) -> bool: if not r.get("country") or not r.get("revenue"): return False @@ -70,16 +74,32 @@ def is_valid(r: dict[str, str]) -> bool: ) +def export_stdout(summary: Summary) -> None: + print(summary.to_text()) + + +def export_file(summary: Summary, path: Path) -> None: + path.write_text(summary.to_text() + "\n") + print(f"Wrote report to {path.resolve()}") + + +def export_json(summary: Summary, path: Path) -> None: + + data = summary.to_json() + path.write_text(json.dumps(data, indent=2)) + print(f"Wrote report to {path.resolve()}") + + def main() -> None: config = ReportConfig(country="NL", min_revenue=10.0) rows = load_data(Path("sales.csv"), config) summary = summarize(rows, config) # Option 1: print to console - summary.export_stdout() + export_stdout(summary) # Option 2: write to file - summary.export_file(Path("report.txt")) + export_file(summary, Path("report.txt")) if __name__ == "__main__": From a2d97a92b1c4821e278f2c1f892768a456a4fd93 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 5 Mar 2026 17:01:52 +0100 Subject: [PATCH 098/113] Small code cleanup --- 2026/clean/report_after.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/2026/clean/report_after.py b/2026/clean/report_after.py index 7dfefa21..4b3cfd83 100644 --- a/2026/clean/report_after.py +++ b/2026/clean/report_after.py @@ -84,7 +84,6 @@ def export_file(summary: Summary, path: Path) -> None: def export_json(summary: Summary, path: Path) -> None: - data = summary.to_json() path.write_text(json.dumps(data, indent=2)) print(f"Wrote report to {path.resolve()}") @@ -101,6 +100,9 @@ def main() -> None: # Option 2: write to file export_file(summary, Path("report.txt")) + # Option 3: write to JSON file + export_json(summary, Path("report.json")) + if __name__ == "__main__": main() From 102b4f626730ca4f352a387f340d6293dbb55264 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 17 Mar 2026 14:25:12 +0100 Subject: [PATCH 099/113] Added type checking option to settings. --- .vscode/settings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ce291017..f0bed7bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,10 @@ { - // Python settings - // "python.envFile": "${workspaceFolder}/.env",, - // Test settings - "python.testing.pytestEnabled": true, + // Python settings + // "python.envFile": "${workspaceFolder}/.env",, + // Test settings + "python.testing.pytestEnabled": true, // "python.testing.unittestEnabled": false, - "python.testing.cwd": "${workspaceFolder}/tests", - "python.defaultInterpreterPath": "/Users/todd/.pyenv/shims/python", - "python.analysis.typeCheckingMode": "basic" + "python.testing.cwd": "${workspaceFolder}/tests", + "python.defaultInterpreterPath": "${workspaceFolder}/2025/typescript/lokalise/.venv/bin/python", + // "python.analysis.typeCheckingMode": "off" } From f2b9a404561e0069a06375efa03741c680efe2ca Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Mar 2026 16:56:18 +0100 Subject: [PATCH 100/113] Added DRY code examples. --- 2026/dry/after.py | 47 ++++++++++++++++++++++++++++++++ 2026/dry/bad_refactor.py | 59 ++++++++++++++++++++++++++++++++++++++++ 2026/dry/before.py | 40 +++++++++++++++++++++++++++ 2026/dry/pyproject.toml | 6 ++++ 4 files changed, 152 insertions(+) create mode 100644 2026/dry/after.py create mode 100644 2026/dry/bad_refactor.py create mode 100644 2026/dry/before.py create mode 100644 2026/dry/pyproject.toml diff --git a/2026/dry/after.py b/2026/dry/after.py new file mode 100644 index 00000000..4316ac0f --- /dev/null +++ b/2026/dry/after.py @@ -0,0 +1,47 @@ +def is_valid_email(address: str) -> bool: + address = address.strip().lower() + return "@" in address + + +def normalize_email(address: str) -> str: + address = address.strip().lower() + local_part, domain = address.split("@", maxsplit=1) + domain = domain.removeprefix("mail.") + return f"{local_part}@{domain}" + + +def is_valid_username(username: str) -> bool: + username = username.strip().lower() + return len(username) >= 3 + + +def normalize_username(username: str) -> str: + username = username.strip().lower() + return username.removeprefix("@") + + +def normalize_email_addresses(addresses: list[str]) -> list[str]: + return [normalize_email(a) for a in addresses if is_valid_email(a)] + + +def normalize_usernames(usernames: list[str]) -> list[str]: + return [normalize_username(u) for u in usernames if is_valid_username(u)] + + +def main() -> None: + email_addresses = [ + "Example@Mail.arjancodes.com", + "Test@hotmail.com", + "User@live.com", + "not-an-email", + ] + normalized_emails = normalize_email_addresses(email_addresses) + print(normalized_emails) + + usernames = [" @ExampleUser ", " @TestUser ", " @User ", " @U "] + normalized_usernames = normalize_usernames(usernames) + print(normalized_usernames) + + +if __name__ == "__main__": + main() diff --git a/2026/dry/bad_refactor.py b/2026/dry/bad_refactor.py new file mode 100644 index 00000000..9ec91082 --- /dev/null +++ b/2026/dry/bad_refactor.py @@ -0,0 +1,59 @@ +def normalize_strings( + items: list[str], + require_at_symbol: bool = False, + min_length: int = 0, + remove_prefix: str = "", + strip_mail_prefix: bool = False, + split_at_symbol: bool = False, + lowercase: bool = True, + strip_whitespace: bool = True, +) -> list[str]: + result: list[str] = [] + + for item in items: + if strip_whitespace: + item = item.strip() + if lowercase: + item = item.lower() + if require_at_symbol and "@" not in item: + continue + if len(item) < min_length: + continue + if remove_prefix: + item = item.removeprefix(remove_prefix) + if split_at_symbol: + local_part, domain = item.split("@", maxsplit=1) + if strip_mail_prefix: + domain = domain.removeprefix("mail.") + item = f"{local_part}@{domain}" + result.append(item) + + return result + + +def main() -> None: + email_addresses = [ + "Example@Mail.arjancodes.com", + "Test@hotmail.com", + "User@live.com", + "not-an-email", + ] + normalized_emails = normalize_strings( + email_addresses, + require_at_symbol=True, + split_at_symbol=True, + strip_mail_prefix=True, + ) + print(normalized_emails) + + usernames = [" @ExampleUser ", " @TestUser ", " @User ", " @U "] + normalized_usernames = normalize_strings( + usernames, + min_length=3, + remove_prefix="@", + ) + print(normalized_usernames) + + +if __name__ == "__main__": + main() diff --git a/2026/dry/before.py b/2026/dry/before.py new file mode 100644 index 00000000..cb086e35 --- /dev/null +++ b/2026/dry/before.py @@ -0,0 +1,40 @@ +def normalize_email_addresses(addresses: list[str]) -> list[str]: + result: list[str] = [] + for address in addresses: + address = address.strip().lower() + if "@" not in address: + continue + local_part, domain = address.split("@", maxsplit=1) + domain = domain.removeprefix("mail.") + result.append(f"{local_part}@{domain}") + return result + + +def normalize_usernames(usernames: list[str]) -> list[str]: + result: list[str] = [] + for username in usernames: + username = username.strip().lower() + if len(username) < 3: + continue + username = username.removeprefix("@") + result.append(username) + return result + + +def main() -> None: + email_addresses = [ + "Example@Mail.arjancodes.com", + "Test@hotmail.com", + "User@live.com", + "not-an-email", + ] + normalized_emails = normalize_email_addresses(email_addresses) + print(normalized_emails) + + usernames = [" @ExampleUser ", " @TestUser ", " @User ", " @U "] + normalized_usernames = normalize_usernames(usernames) + print(normalized_usernames) + + +if __name__ == "__main__": + main() diff --git a/2026/dry/pyproject.toml b/2026/dry/pyproject.toml new file mode 100644 index 00000000..6f3e185d --- /dev/null +++ b/2026/dry/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "dry" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] From 0d637c00c92c007217e4892e8ba0d3bf49a66d7e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Mar 2026 15:39:24 +0100 Subject: [PATCH 101/113] Added custom data example --- 2026/apidata/database.py | 25 ++++ 2026/apidata/db_models.py | 79 ++++++++++ 2026/apidata/main.py | 21 +++ 2026/apidata/pyproject.toml | 10 ++ 2026/apidata/routes.py | 175 ++++++++++++++++++++++ 2026/apidata/schemas.py | 72 +++++++++ 2026/apidata/uv.lock | 283 ++++++++++++++++++++++++++++++++++++ 7 files changed, 665 insertions(+) create mode 100644 2026/apidata/database.py create mode 100644 2026/apidata/db_models.py create mode 100644 2026/apidata/main.py create mode 100644 2026/apidata/pyproject.toml create mode 100644 2026/apidata/routes.py create mode 100644 2026/apidata/schemas.py create mode 100644 2026/apidata/uv.lock diff --git a/2026/apidata/database.py b/2026/apidata/database.py new file mode 100644 index 00000000..974a4d5e --- /dev/null +++ b/2026/apidata/database.py @@ -0,0 +1,25 @@ +from typing import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +DATABASE_URL = "sqlite:///./shop.db" + +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False}, +) + +SessionLocal = sessionmaker( + bind=engine, + autoflush=False, + autocommit=False, +) + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/2026/apidata/db_models.py b/2026/apidata/db_models.py new file mode 100644 index 00000000..8a0aab6e --- /dev/null +++ b/2026/apidata/db_models.py @@ -0,0 +1,79 @@ +from typing import Any + +from sqlalchemy import JSON, ForeignKey, String +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + custom_data: Mapped[dict[str, Any]] = mapped_column( + MutableDict.as_mutable(JSON), + default=dict, + nullable=False, + ) + + def apply_custom_data_patch(self, patch: dict[str, Any]) -> None: + """ + Stripe-style behavior: + - {} clears all custom_data + - {"key": value} sets or updates a key + - {"key": None} removes a key + """ + if patch == {}: + self.custom_data = {} + return + + current = dict(self.custom_data or {}) + + for key, value in patch.items(): + if value is None: + current.pop(key, None) + else: + current[key] = value + + self.custom_data = current + + def apply_patch(self, data: dict[str, Any]) -> None: + for key, value in data.items(): + if value is None and key != "custom_data": + continue + + if key == "custom_data": + self.apply_custom_data_patch(value) + else: + setattr(self, key, value) + + +class Customer(Base): + __tablename__ = "customers" + + id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(String(255), unique=True, index=True) + name: Mapped[str] = mapped_column(String(100)) + + orders: Mapped[list["Order"]] = relationship(back_populates="customer") + + +class Product(Base): + __tablename__ = "products" + + id: Mapped[int] = mapped_column(primary_key=True) + sku: Mapped[str] = mapped_column(String(64), unique=True, index=True) + name: Mapped[str] = mapped_column(String(120)) + price_cents: Mapped[int] + + orders: Mapped[list["Order"]] = relationship(back_populates="product") + + +class Order(Base): + __tablename__ = "orders" + + id: Mapped[int] = mapped_column(primary_key=True) + customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"), index=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True) + quantity: Mapped[int] = mapped_column(default=1) + status: Mapped[str] = mapped_column(String(32), default="pending") + total_cents: Mapped[int] + + customer: Mapped[Customer] = relationship(back_populates="orders") + product: Mapped[Product] = relationship(back_populates="orders") diff --git a/2026/apidata/main.py b/2026/apidata/main.py new file mode 100644 index 00000000..180d4132 --- /dev/null +++ b/2026/apidata/main.py @@ -0,0 +1,21 @@ +# shop_api/main.py +from __future__ import annotations + +from database import engine +from db_models import Base +from fastapi import FastAPI +from routes import router + + +def create_app() -> FastAPI: + app = FastAPI(title="Shop API with custom_data") + app.include_router(router) + return app + + +def main() -> FastAPI: + Base.metadata.create_all(bind=engine) + return create_app() + + +app = main() diff --git a/2026/apidata/pyproject.toml b/2026/apidata/pyproject.toml new file mode 100644 index 00000000..313a1dea --- /dev/null +++ b/2026/apidata/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "apidata" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "fastapi>=0.135.1", + "pydantic[email]>=2.12.5", + "sqlalchemy>=2.0.48", + "uvicorn>=0.42.0", +] diff --git a/2026/apidata/routes.py b/2026/apidata/routes.py new file mode 100644 index 00000000..a2886ed9 --- /dev/null +++ b/2026/apidata/routes.py @@ -0,0 +1,175 @@ +from database import get_db +from db_models import Customer, Order, Product +from fastapi import APIRouter, Depends, HTTPException, Response, status +from schemas import ( + CustomerCreate, + CustomerPatch, + CustomerRead, + OrderCreate, + OrderPatch, + OrderRead, + ProductCreate, + ProductPatch, + ProductRead, +) +from sqlalchemy import select +from sqlalchemy.orm import Session + +router = APIRouter() + + +@router.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@router.post( + "/customers", response_model=CustomerRead, status_code=status.HTTP_201_CREATED +) +def create_customer(payload: CustomerCreate, db: Session = Depends(get_db)) -> Customer: + existing = db.scalar(select(Customer).where(Customer.email == str(payload.email))) + if existing is not None: + raise HTTPException( + status_code=409, detail="Customer with this email already exists." + ) + + customer = Customer(**payload.model_dump()) + db.add(customer) + db.commit() + db.refresh(customer) + return customer + + +@router.get("/customers/{customer_id}", response_model=CustomerRead) +def get_customer(customer_id: int, db: Session = Depends(get_db)) -> Customer: + customer = db.get(Customer, customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + return customer + + +@router.patch("/customers/{customer_id}", response_model=CustomerRead) +def patch_customer( + customer_id: int, + payload: CustomerPatch, + db: Session = Depends(get_db), +) -> Customer: + customer = db.get(Customer, customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + + customer.apply_patch(payload.model_dump(exclude_unset=True)) + + db.commit() + db.refresh(customer) + return customer + + +@router.post( + "/products", response_model=ProductRead, status_code=status.HTTP_201_CREATED +) +def create_product(payload: ProductCreate, db: Session = Depends(get_db)) -> Product: + existing = db.scalar(select(Product).where(Product.sku == payload.sku)) + if existing is not None: + raise HTTPException( + status_code=409, detail="Product with this SKU already exists." + ) + + product = Product(**payload.model_dump()) + db.add(product) + db.commit() + db.refresh(product) + return product + + +@router.get("/products/{product_id}", response_model=ProductRead) +def get_product(product_id: int, db: Session = Depends(get_db)) -> Product: + product = db.get(Product, product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + return product + + +@router.patch("/products/{product_id}", response_model=ProductRead) +def patch_product( + product_id: int, + payload: ProductPatch, + db: Session = Depends(get_db), +) -> Product: + product = db.get(Product, product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + + product.apply_patch(payload.model_dump(exclude_unset=True)) + + db.commit() + db.refresh(product) + return product + + +@router.post("/orders", response_model=OrderRead, status_code=status.HTTP_201_CREATED) +def create_order(payload: OrderCreate, db: Session = Depends(get_db)) -> Order: + customer = db.get(Customer, payload.customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + + product = db.get(Product, payload.product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + + data = payload.model_dump() + + order = Order( + **data, + total_cents=product.price_cents * data["quantity"], + ) + + db.add(order) + db.commit() + db.refresh(order) + return order + + +@router.get("/orders/{order_id}", response_model=OrderRead) +def get_order(order_id: int, db: Session = Depends(get_db)) -> Order: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + return order + + +@router.patch("/orders/{order_id}", response_model=OrderRead) +def patch_order( + order_id: int, + payload: OrderPatch, + db: Session = Depends(get_db), +) -> Order: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + + updates = payload.model_dump(exclude_unset=True) + + if "quantity" in updates: + product = db.get(Product, order.product_id) + if product is None: + raise HTTPException(status_code=500, detail="Related product not found.") + order.quantity = updates.pop("quantity") + order.total_cents = product.price_cents * order.quantity + + order.apply_patch(updates) + + db.commit() + db.refresh(order) + return order + + +@router.delete("/orders/{order_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_order(order_id: int, db: Session = Depends(get_db)) -> Response: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + + db.delete(order) + db.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/2026/apidata/schemas.py b/2026/apidata/schemas.py new file mode 100644 index 00000000..a1d5c01c --- /dev/null +++ b/2026/apidata/schemas.py @@ -0,0 +1,72 @@ +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, EmailStr, Field + + +class CustomDataSchema(BaseModel): + custom_data: dict[str, Any] = Field(default_factory=dict) + + +class CustomerCreate(CustomDataSchema): + email: EmailStr + name: str = Field(min_length=1, max_length=100) + + +class CustomerPatch(BaseModel): + email: EmailStr | None = None + name: str | None = Field(default=None, min_length=1, max_length=100) + custom_data: dict[str, Any] | None = None + + +class CustomerRead(CustomDataSchema): + model_config = ConfigDict(from_attributes=True) + + id: int + email: EmailStr + name: str + + +class ProductCreate(CustomDataSchema): + sku: str = Field(min_length=1, max_length=64) + name: str = Field(min_length=1, max_length=120) + price_cents: int = Field(ge=0) + + +class ProductPatch(BaseModel): + sku: str | None = Field(default=None, min_length=1, max_length=64) + name: str | None = Field(default=None, min_length=1, max_length=120) + price_cents: int | None = Field(default=None, ge=0) + custom_data: dict[str, Any] | None = None + + +class ProductRead(CustomDataSchema): + model_config = ConfigDict(from_attributes=True) + + id: int + sku: str + name: str + price_cents: int + + +class OrderCreate(CustomDataSchema): + customer_id: int + product_id: int + quantity: int = Field(default=1, ge=1) + status: Literal["pending", "paid", "shipped", "cancelled"] = "pending" + + +class OrderPatch(BaseModel): + quantity: int | None = Field(default=None, ge=1) + status: Literal["pending", "paid", "shipped", "cancelled"] | None = None + custom_data: dict[str, Any] | None = None + + +class OrderRead(CustomDataSchema): + model_config = ConfigDict(from_attributes=True) + + id: int + customer_id: int + product_id: int + quantity: int + status: str + total_cents: int diff --git a/2026/apidata/uv.lock b/2026/apidata/uv.lock new file mode 100644 index 00000000..c2be7e3e --- /dev/null +++ b/2026/apidata/uv.lock @@ -0,0 +1,283 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "apidata" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic", extra = ["email"] }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.135.1" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.12.5" }, + { name = "sqlalchemy", specifier = ">=2.0.48" }, + { name = "uvicorn", specifier = ">=0.42.0" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] From 5ec4f3533f9ea38621da3347ed6843408358797c Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Mar 2026 15:56:02 +0100 Subject: [PATCH 102/113] Added baseline examples. --- 2026/apidata/baseline/db_models.py | 41 +++++++ 2026/apidata/baseline/routes.py | 180 +++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 2026/apidata/baseline/db_models.py create mode 100644 2026/apidata/baseline/routes.py diff --git a/2026/apidata/baseline/db_models.py b/2026/apidata/baseline/db_models.py new file mode 100644 index 00000000..464c25d5 --- /dev/null +++ b/2026/apidata/baseline/db_models.py @@ -0,0 +1,41 @@ +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +class Customer(Base): + __tablename__ = "customers" + + id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(String(255), unique=True, index=True) + name: Mapped[str] = mapped_column(String(100)) + + orders: Mapped[list["Order"]] = relationship(back_populates="customer") + + +class Product(Base): + __tablename__ = "products" + + id: Mapped[int] = mapped_column(primary_key=True) + sku: Mapped[str] = mapped_column(String(64), unique=True, index=True) + name: Mapped[str] = mapped_column(String(120)) + price_cents: Mapped[int] + + orders: Mapped[list["Order"]] = relationship(back_populates="product") + + +class Order(Base): + __tablename__ = "orders" + + id: Mapped[int] = mapped_column(primary_key=True) + customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"), index=True) + product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True) + quantity: Mapped[int] = mapped_column(default=1) + status: Mapped[str] = mapped_column(String(32), default="pending") + total_cents: Mapped[int] + + customer: Mapped[Customer] = relationship(back_populates="orders") + product: Mapped[Product] = relationship(back_populates="orders") diff --git a/2026/apidata/baseline/routes.py b/2026/apidata/baseline/routes.py new file mode 100644 index 00000000..6aac8ea1 --- /dev/null +++ b/2026/apidata/baseline/routes.py @@ -0,0 +1,180 @@ +from database import get_db +from db_models import Customer, Order, Product +from fastapi import APIRouter, Depends, HTTPException, Response, status +from schemas import ( + CustomerCreate, + CustomerPatch, + CustomerRead, + OrderCreate, + OrderPatch, + OrderRead, + ProductCreate, + ProductPatch, + ProductRead, +) +from sqlalchemy import select +from sqlalchemy.orm import Session + +router = APIRouter() + + +@router.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@router.post( + "/customers", response_model=CustomerRead, status_code=status.HTTP_201_CREATED +) +def create_customer(payload: CustomerCreate, db: Session = Depends(get_db)) -> Customer: + existing = db.scalar(select(Customer).where(Customer.email == str(payload.email))) + if existing is not None: + raise HTTPException( + status_code=409, detail="Customer with this email already exists." + ) + + customer = Customer(**payload.model_dump()) + db.add(customer) + db.commit() + db.refresh(customer) + return customer + + +@router.get("/customers/{customer_id}", response_model=CustomerRead) +def get_customer(customer_id: int, db: Session = Depends(get_db)) -> Customer: + customer = db.get(Customer, customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + return customer + + +@router.patch("/customers/{customer_id}", response_model=CustomerRead) +def patch_customer( + customer_id: int, + payload: CustomerPatch, + db: Session = Depends(get_db), +) -> Customer: + customer = db.get(Customer, customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + + updates = payload.model_dump(exclude_unset=True) + for key, value in updates.items(): + setattr(customer, key, value) + + db.commit() + db.refresh(customer) + return customer + + +@router.post( + "/products", response_model=ProductRead, status_code=status.HTTP_201_CREATED +) +def create_product(payload: ProductCreate, db: Session = Depends(get_db)) -> Product: + existing = db.scalar(select(Product).where(Product.sku == payload.sku)) + if existing is not None: + raise HTTPException( + status_code=409, detail="Product with this SKU already exists." + ) + + product = Product(**payload.model_dump()) + db.add(product) + db.commit() + db.refresh(product) + return product + + +@router.get("/products/{product_id}", response_model=ProductRead) +def get_product(product_id: int, db: Session = Depends(get_db)) -> Product: + product = db.get(Product, product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + return product + + +@router.patch("/products/{product_id}", response_model=ProductRead) +def patch_product( + product_id: int, + payload: ProductPatch, + db: Session = Depends(get_db), +) -> Product: + product = db.get(Product, product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + + updates = payload.model_dump(exclude_unset=True) + for key, value in updates.items(): + setattr(product, key, value) + + db.commit() + db.refresh(product) + return product + + +@router.post("/orders", response_model=OrderRead, status_code=status.HTTP_201_CREATED) +def create_order(payload: OrderCreate, db: Session = Depends(get_db)) -> Order: + customer = db.get(Customer, payload.customer_id) + if customer is None: + raise HTTPException(status_code=404, detail="Customer not found.") + + product = db.get(Product, payload.product_id) + if product is None: + raise HTTPException(status_code=404, detail="Product not found.") + + data = payload.model_dump() + + order = Order( + **data, + total_cents=product.price_cents * data["quantity"], + ) + + db.add(order) + db.commit() + db.refresh(order) + return order + + +@router.get("/orders/{order_id}", response_model=OrderRead) +def get_order(order_id: int, db: Session = Depends(get_db)) -> Order: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + return order + + +@router.patch("/orders/{order_id}", response_model=OrderRead) +def patch_order( + order_id: int, + payload: OrderPatch, + db: Session = Depends(get_db), +) -> Order: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + + updates = payload.model_dump(exclude_unset=True) + + if "quantity" in updates: + product = db.get(Product, order.product_id) + if product is None: + raise HTTPException(status_code=500, detail="Related product not found.") + order.quantity = updates.pop("quantity") + order.total_cents = product.price_cents * order.quantity + + for key, value in updates.items(): + setattr(order, key, value) + + db.commit() + db.refresh(order) + return order + + +@router.delete("/orders/{order_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_order(order_id: int, db: Session = Depends(get_db)) -> Response: + order = db.get(Order, order_id) + if order is None: + raise HTTPException(status_code=404, detail="Order not found.") + + db.delete(order) + db.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) From ec1d33e6ae991b24a4a56974e98bedb01f5dd6a3 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Mar 2026 15:59:13 +0100 Subject: [PATCH 103/113] Simplified main file --- 2026/apidata/main.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/2026/apidata/main.py b/2026/apidata/main.py index 180d4132..a787ba13 100644 --- a/2026/apidata/main.py +++ b/2026/apidata/main.py @@ -1,21 +1,9 @@ -# shop_api/main.py -from __future__ import annotations - from database import engine from db_models import Base from fastapi import FastAPI from routes import router +Base.metadata.create_all(bind=engine) -def create_app() -> FastAPI: - app = FastAPI(title="Shop API with custom_data") - app.include_router(router) - return app - - -def main() -> FastAPI: - Base.metadata.create_all(bind=engine) - return create_app() - - -app = main() +app = FastAPI(title="Shop API") +app.include_router(router) From d61238af493b872b2cdf8eefa97e706fe859309d Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 18 Mar 2026 13:46:45 +0100 Subject: [PATCH 104/113] Added policy examples. --- 2026/policy/01_messy.py | 44 ++++++++++++++ 2026/policy/02_oop.py | 81 +++++++++++++++++++++++++ 2026/policy/03_final.py | 111 ++++++++++++++++++++++++++++++++++ 2026/policy/domain.py | 20 ++++++ 2026/policy/pyproject.toml | 7 +++ 2026/policy/settings.env | 1 + 2026/policy/uv.lock | 121 +++++++++++++++++++++++++++++++++++++ 7 files changed, 385 insertions(+) create mode 100644 2026/policy/01_messy.py create mode 100644 2026/policy/02_oop.py create mode 100644 2026/policy/03_final.py create mode 100644 2026/policy/domain.py create mode 100644 2026/policy/pyproject.toml create mode 100644 2026/policy/settings.env create mode 100644 2026/policy/uv.lock diff --git a/2026/policy/01_messy.py b/2026/policy/01_messy.py new file mode 100644 index 00000000..164bea0d --- /dev/null +++ b/2026/policy/01_messy.py @@ -0,0 +1,44 @@ +from domain import Request, User + + +def process_request(user: User, request: Request) -> Request: + if not user.is_active: + raise PermissionError("Inactive users cannot make requests") + + if request.action == "delete" and not user.has_mfa: + raise PermissionError("MFA is required for delete actions") + + if request.required_role and request.required_role not in user.roles: + raise PermissionError(f"Missing required role: {request.required_role}") + + if request.requires_audit: + request.audit_log.append( + f"{user.name} performed {request.action} on {request.path}" + ) + + request.access_granted = True + return request + + +def main() -> None: + user = User( + name="Arjan", + is_active=True, + roles={"admin"}, + has_mfa=True, + subscription_tier="pro", + ) + + request = Request( + path="/admin/users", + action="delete", + requires_audit=True, + required_role="admin", + ) + + result = process_request(user, request) + print(result) + + +if __name__ == "__main__": + main() diff --git a/2026/policy/02_oop.py b/2026/policy/02_oop.py new file mode 100644 index 00000000..e2041160 --- /dev/null +++ b/2026/policy/02_oop.py @@ -0,0 +1,81 @@ +from typing import Protocol + +from domain import Request, User + + +class RequestPolicy(Protocol): + def apply(self, user: User, request: Request) -> None: ... + + +class ActiveUserPolicy: + def apply(self, user: User, request: Request) -> None: + if not user.is_active: + raise PermissionError("Inactive users cannot make requests") + + +class MfaRequiredPolicy: + def apply(self, user: User, request: Request) -> None: + if request.action == "delete" and not user.has_mfa: + raise PermissionError("MFA is required for delete actions") + + +class RoleRequiredPolicy: + def apply(self, user: User, request: Request) -> None: + if request.required_role and request.required_role not in user.roles: + raise PermissionError(f"Missing required role: {request.required_role}") + + +class AuditPolicy: + def apply(self, user: User, request: Request) -> None: + if request.requires_audit: + request.audit_log.append( + f"{user.name} performed {request.action} on {request.path}" + ) + + +class GrantAccessPolicy: + def apply(self, user: User, request: Request) -> None: + request.access_granted = True + + +class CompositePolicy: + def __init__(self, policies: list[RequestPolicy]) -> None: + self.policies = policies + + def apply(self, user: User, request: Request) -> None: + for policy in self.policies: + policy.apply(user, request) + + +def main() -> None: + user = User( + name="Arjan", + is_active=True, + roles={"admin"}, + has_mfa=True, + subscription_tier="pro", + ) + + request = Request( + path="/admin/users", + action="delete", + requires_audit=True, + required_role="admin", + ) + + policy = CompositePolicy( + [ + ActiveUserPolicy(), + MfaRequiredPolicy(), + RoleRequiredPolicy(), + AuditPolicy(), + GrantAccessPolicy(), + ] + ) + + policy.apply(user, request) + print(request) + + +if __name__ == "__main__": + main() diff --git a/2026/policy/03_final.py b/2026/policy/03_final.py new file mode 100644 index 00000000..473056ed --- /dev/null +++ b/2026/policy/03_final.py @@ -0,0 +1,111 @@ +from dataclasses import replace +from functools import reduce +from typing import Callable + +from domain import Request, User +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +Policy = Callable[[User, Request], Request] + + +class Settings(BaseSettings): + enabled_policies: list[str] = Field( + default_factory=lambda: [ + "active_user", + "role_required", + "mfa_required", + "grant_access", + ] + ) + + model_config = SettingsConfigDict( + env_file="settings.env", + env_file_encoding="utf-8", + env_prefix="APP_", + ) + + +def active_user(user: User, request: Request) -> Request: + if not user.is_active: + raise PermissionError("Inactive users cannot make requests") + return request + + +def mfa_required(user: User, request: Request) -> Request: + if request.action == "delete" and not user.has_mfa: + raise PermissionError("MFA is required for delete actions") + return request + + +def role_required(user: User, request: Request) -> Request: + if request.required_role and request.required_role not in user.roles: + raise PermissionError(f"Missing required role: {request.required_role}") + return request + + +def grant_access(user: User, request: Request) -> Request: + return replace(request, access_granted=True) + + +def audit(user: User, request: Request) -> Request: + if not request.requires_audit: + return request + + return replace( + request, + audit_log=request.audit_log + + [f"{user.name} performed {request.action} on {request.path}"], + ) + + +POLICY_REGISTRY: dict[str, Policy] = { + "active_user": active_user, + "mfa_required": mfa_required, + "role_required": role_required, + "grant_access": grant_access, + "audit": audit, +} + + +def get_policies(settings: Settings) -> list[Policy]: + try: + return [POLICY_REGISTRY[name] for name in settings.enabled_policies] + except KeyError as exc: + valid = ", ".join(sorted(POLICY_REGISTRY)) + raise ValueError( + f"Unknown policy name: {exc.args[0]!r}. Valid names: {valid}" + ) from exc + + +def apply_policies(user: User, request: Request, policies: list[Policy]) -> Request: + return reduce(lambda current, policy: policy(user, current), policies, request) + + +def main() -> None: + settings = Settings() + + user = User( + name="Arjan", + is_active=True, + roles={"admin"}, + has_mfa=True, + subscription_tier="pro", + ) + + request = Request( + path="/admin/users", + action="delete", + requires_audit=True, + required_role="admin", + ) + + policies = get_policies(settings) + result = apply_policies(user, request, policies) + + print("Enabled policies:", settings.enabled_policies) + print(result) + + +if __name__ == "__main__": + main() diff --git a/2026/policy/domain.py b/2026/policy/domain.py new file mode 100644 index 00000000..7af2568a --- /dev/null +++ b/2026/policy/domain.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass, field + + +@dataclass(slots=True) +class User: + name: str + is_active: bool + roles: set[str] + has_mfa: bool + subscription_tier: str + + +@dataclass(slots=True) +class Request: + path: str + action: str + requires_audit: bool = False + required_role: str | None = None + audit_log: list[str] = field(default_factory=list[str]) + access_granted: bool = False diff --git a/2026/policy/pyproject.toml b/2026/policy/pyproject.toml new file mode 100644 index 00000000..2c6c06fb --- /dev/null +++ b/2026/policy/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "policy" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "pydantic-settings>=2.13.1", +] diff --git a/2026/policy/settings.env b/2026/policy/settings.env new file mode 100644 index 00000000..0d4072c8 --- /dev/null +++ b/2026/policy/settings.env @@ -0,0 +1 @@ +APP_ENABLED_POLICIES=["active_user","role_required","mfa_required","grant_access"] \ No newline at end of file diff --git a/2026/policy/uv.lock b/2026/policy/uv.lock new file mode 100644 index 00000000..9de84c36 --- /dev/null +++ b/2026/policy/uv.lock @@ -0,0 +1,121 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "policy" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic-settings" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic-settings", specifier = ">=2.13.1" }] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] From fbfe9d16953a378c4535631c47e5e1f93bafd618 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 17 Mar 2026 15:36:40 +0100 Subject: [PATCH 105/113] Added code example. --- 2026/generators/00_basic.py | 21 +++++++ 2026/generators/01_structured.py | 51 +++++++++++++++++ 2026/generators/02_composition.py | 86 ++++++++++++++++++++++++++++ 2026/generators/03_backpressure.py | 92 ++++++++++++++++++++++++++++++ 2026/generators/04_send.py | 70 +++++++++++++++++++++++ 2026/generators/04b_return.py | 50 ++++++++++++++++ 2026/generators/05_async.py | 82 ++++++++++++++++++++++++++ 2026/generators/pyproject.toml | 6 ++ 2026/generators/uv.lock | 8 +++ 9 files changed, 466 insertions(+) create mode 100644 2026/generators/00_basic.py create mode 100644 2026/generators/01_structured.py create mode 100644 2026/generators/02_composition.py create mode 100644 2026/generators/03_backpressure.py create mode 100644 2026/generators/04_send.py create mode 100644 2026/generators/04b_return.py create mode 100644 2026/generators/05_async.py create mode 100644 2026/generators/pyproject.toml create mode 100644 2026/generators/uv.lock diff --git a/2026/generators/00_basic.py b/2026/generators/00_basic.py new file mode 100644 index 00000000..617fce62 --- /dev/null +++ b/2026/generators/00_basic.py @@ -0,0 +1,21 @@ +from typing import Generator + + +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + print(f"producing: {line}") + yield line + + +def main(): + for line in read_logs(): + print(f"consuming: {line}") + + +if __name__ == "__main__": + main() diff --git a/2026/generators/01_structured.py b/2026/generators/01_structured.py new file mode 100644 index 00000000..0ea5df33 --- /dev/null +++ b/2026/generators/01_structured.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass +from enum import StrEnum, auto +from typing import Generator, Iterable + + +# --- Domain model --- +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +# --- Source generator --- +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + print(f"producing: {line}") + yield line + + +# --- Transformation step --- +def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]: + for line in lines: + level_text, message = line.split(" ", maxsplit=1) + level = LogLevel(level_text) # validated conversion + yield LogRecord(level=level, message=message) + + +def handle_records(records: Iterable[LogRecord]) -> None: + for record in records: + print(f"handling: {record}") + + +# --- Application entry point --- +def main() -> None: + records = parse_logs(read_logs()) + handle_records(records) + + +if __name__ == "__main__": + main() diff --git a/2026/generators/02_composition.py b/2026/generators/02_composition.py new file mode 100644 index 00000000..6d96c2b4 --- /dev/null +++ b/2026/generators/02_composition.py @@ -0,0 +1,86 @@ +from dataclasses import dataclass +from enum import StrEnum, auto +from functools import reduce +from typing import Any, Callable, Generator, Iterable + + +# --- Domain model --- +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +# --- Source generator --- +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + print(f"producing: {line}") + yield line + + +# --- Pipeline stages --- +def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]: + for line in lines: + level_text, message = line.split(" ", maxsplit=1) + level = LogLevel(level_text) + yield LogRecord(level=level, message=message) + + +def filter_important( + records: Iterable[LogRecord], +) -> Generator[LogRecord, None, None]: + for record in records: + if record.level in {LogLevel.WARNING, LogLevel.ERROR}: + yield record + + +def normalize_messages( + records: Iterable[LogRecord], +) -> Generator[LogRecord, None, None]: + for record in records: + yield LogRecord( + level=record.level, + message=record.message.lower(), + ) + + +# --- Composition helper --- +type PipelineStage = Callable[[Iterable[Any]], Iterable[Any]] + + +def compose(*stages: PipelineStage) -> PipelineStage: + def apply(data: Iterable[Any]) -> Iterable[Any]: + return reduce(lambda acc, stage: stage(acc), stages, data) + + return apply + + +def handle_records(records: Iterable[LogRecord]) -> None: + for record in records: + print(f"handling: {record}") + + +# --- Application entry point --- +def main() -> None: + pipeline = compose( + parse_logs, + filter_important, + normalize_messages, + ) + + handle_records(pipeline(read_logs())) + + +if __name__ == "__main__": + main() diff --git a/2026/generators/03_backpressure.py b/2026/generators/03_backpressure.py new file mode 100644 index 00000000..aa39766d --- /dev/null +++ b/2026/generators/03_backpressure.py @@ -0,0 +1,92 @@ +import time +from dataclasses import dataclass +from enum import StrEnum, auto +from functools import reduce +from typing import Any, Callable, Generator, Iterable + + +# --- Domain model --- +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +# --- Source generator --- +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + print(f"producing: {line}") + yield line + + +# --- Pipeline stages --- +def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]: + for line in lines: + print(f"parsing: {line}") + level_text, message = line.split(" ", maxsplit=1) + level = LogLevel(level_text) + yield LogRecord(level=level, message=message) + + +def filter_important( + records: Iterable[LogRecord], +) -> Generator[LogRecord, None, None]: + for record in records: + print(f"filtering: {record}") + if record.level in {LogLevel.WARNING, LogLevel.ERROR}: + yield record + + +def normalize_messages( + records: Iterable[LogRecord], +) -> Generator[LogRecord, None, None]: + for record in records: + print(f"normalizing: {record}") + yield LogRecord( + level=record.level, + message=record.message.lower(), + ) + + +# --- Composition helper --- +type PipelineStage = Callable[[Iterable[Any]], Iterable[Any]] + + +def compose(*stages: PipelineStage) -> PipelineStage: + def apply(data: Iterable[Any]) -> Iterable[Any]: + return reduce(lambda acc, stage: stage(acc), stages, data) + + return apply + + +# --- Slow consumer --- +def handle_records(records: Iterable[LogRecord]) -> None: + for record in records: + print(f"handling: {record}") + time.sleep(1) + + +# --- Application entry point --- +def main() -> None: + pipeline = compose( + parse_logs, + filter_important, + normalize_messages, + ) + + handle_records(pipeline(read_logs())) + + +if __name__ == "__main__": + main() diff --git a/2026/generators/04_send.py b/2026/generators/04_send.py new file mode 100644 index 00000000..da5addce --- /dev/null +++ b/2026/generators/04_send.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from enum import StrEnum, auto +from typing import Generator, Iterable + + +# --- Domain model --- +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +# --- Source generator --- +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + "warning Disk space low", + ] + for line in lines: + print(f"producing: {line}") + yield line + + +def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]: + for line in lines: + level_text, message = line.split(" ", maxsplit=1) + yield LogRecord(level=LogLevel(level_text), message=message) + + +def should_emit(record: LogRecord, threshold: LogLevel) -> bool: + if threshold is LogLevel.WARNING: + return record.level in {LogLevel.WARNING, LogLevel.ERROR} + return record.level is LogLevel.ERROR + + +# --- Simpler send() example --- +def threshold_filter() -> Generator[LogLevel, LogLevel | None, None]: + threshold = LogLevel.ERROR + while True: + new_threshold = yield threshold + if new_threshold is not None: + threshold = new_threshold + + +def main() -> None: + records = parse_logs(read_logs()) + + filter_settings = threshold_filter() + threshold = next(filter_settings) # prime the generator + + for index, record in enumerate(records): + if index == 2: + threshold = filter_settings.send(LogLevel.WARNING) + else: + threshold = next(filter_settings) + + if should_emit(record, threshold): + print(f"handling: {record}") + + +if __name__ == "__main__": + main() diff --git a/2026/generators/04b_return.py b/2026/generators/04b_return.py new file mode 100644 index 00000000..cc4f051b --- /dev/null +++ b/2026/generators/04b_return.py @@ -0,0 +1,50 @@ +from collections.abc import Generator, Iterable +from dataclasses import dataclass +from enum import StrEnum, auto + + +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +def read_logs() -> Generator[str, None, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + yield line + + +def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, int]: + count = 0 + for line in lines: + level_text, message = line.split(" ", maxsplit=1) + yield LogRecord(level=LogLevel(level_text), message=message) + count += 1 + + return count # ๐Ÿ‘ˆ final result + + +def main() -> None: + parser = parse_logs(read_logs()) + + try: + while True: + record = next(parser) + print(f"handling: {record}") + except StopIteration as e: + print(f"Total records processed: {e.value}") + + +if __name__ == "__main__": + main() diff --git a/2026/generators/05_async.py b/2026/generators/05_async.py new file mode 100644 index 00000000..3b3dedbe --- /dev/null +++ b/2026/generators/05_async.py @@ -0,0 +1,82 @@ +import asyncio +from dataclasses import dataclass +from enum import StrEnum, auto +from typing import AsyncGenerator + + +# --- Domain model --- +class LogLevel(StrEnum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + +@dataclass(slots=True) +class LogRecord: + level: LogLevel + message: str + + +# --- Async source generator --- +async def read_logs() -> AsyncGenerator[str, None]: + lines = [ + "info User logged in", + "warning Slow database query", + "error Payment failed", + ] + for line in lines: + await asyncio.sleep(0.5) + print(f"producing: {line}") + yield line + + +# --- Async pipeline stages --- +async def parse_logs( + lines: AsyncGenerator[str, None], +) -> AsyncGenerator[LogRecord, None]: + async for line in lines: + print(f"parsing: {line}") + level_text, message = line.split(" ", maxsplit=1) + level = LogLevel(level_text) + yield LogRecord(level=level, message=message) + + +async def filter_important( + records: AsyncGenerator[LogRecord, None], +) -> AsyncGenerator[LogRecord, None]: + async for record in records: + print(f"filtering: {record}") + if record.level in {LogLevel.WARNING, LogLevel.ERROR}: + yield record + + +async def normalize_messages( + records: AsyncGenerator[LogRecord, None], +) -> AsyncGenerator[LogRecord, None]: + async for record in records: + print(f"normalizing: {record}") + yield LogRecord( + level=record.level, + message=record.message.lower(), + ) + + +# --- Consumer --- +async def handle_records(records: AsyncGenerator[LogRecord, None]) -> None: + async for record in records: + print(f"handling: {record}") + await asyncio.sleep(1) + + +# --- Application entry point --- +async def main() -> None: + records = read_logs() + parsed = parse_logs(records) + filtered = filter_important(parsed) + normalized = normalize_messages(filtered) + + await handle_records(normalized) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/2026/generators/pyproject.toml b/2026/generators/pyproject.toml new file mode 100644 index 00000000..41e6dee2 --- /dev/null +++ b/2026/generators/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "generators" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] diff --git a/2026/generators/uv.lock b/2026/generators/uv.lock new file mode 100644 index 00000000..c9e747ad --- /dev/null +++ b/2026/generators/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "generators" +version = "0.0.1" +source = { virtual = "." } From 93039e90369476ca9e549c61a03f4a399d4665af Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Wed, 22 Apr 2026 17:20:50 +0200 Subject: [PATCH 106/113] Initial god object code example --- 2026/god/artifacts.py | 41 ++++++++ 2026/god/before.py | 196 ++++++++++++++++++++++++++++++++++++++ 2026/god/config.py | 18 ++++ 2026/god/data_loading.py | 7 ++ 2026/god/evaluation.py | 42 ++++++++ 2026/god/generate_data.py | 84 ++++++++++++++++ 2026/god/main.py | 70 ++++++++++++++ 2026/god/preprocessing.py | 44 +++++++++ 2026/god/pyproject.toml | 10 ++ 2026/god/training.py | 32 +++++++ 2026/god/uv.lock | 190 ++++++++++++++++++++++++++++++++++++ 11 files changed, 734 insertions(+) create mode 100644 2026/god/artifacts.py create mode 100644 2026/god/before.py create mode 100644 2026/god/config.py create mode 100644 2026/god/data_loading.py create mode 100644 2026/god/evaluation.py create mode 100644 2026/god/generate_data.py create mode 100644 2026/god/main.py create mode 100644 2026/god/preprocessing.py create mode 100644 2026/god/pyproject.toml create mode 100644 2026/god/training.py create mode 100644 2026/god/uv.lock diff --git a/2026/god/artifacts.py b/2026/god/artifacts.py new file mode 100644 index 00000000..e7cf1d1e --- /dev/null +++ b/2026/god/artifacts.py @@ -0,0 +1,41 @@ +import json +from pathlib import Path + +import joblib +import pandas as pd +from evaluation import EvaluationResult +from sklearn.ensemble import RandomForestClassifier + + +def save_model(model: RandomForestClassifier, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + joblib.dump(model, path) + + +def save_metrics(result: EvaluationResult, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + data = { + "accuracy": result.accuracy, + "classification_report": result.classification_report, + } + + with path.open("w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +def compute_feature_importances( + model: RandomForestClassifier, + feature_names: list[str], +) -> pd.DataFrame: + return pd.DataFrame( + { + "feature": feature_names, + "importance": model.feature_importances_, + } + ).sort_values("importance", ascending=False) + + +def save_feature_importances(importances: pd.DataFrame, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + importances.to_csv(path, index=False) diff --git a/2026/god/before.py b/2026/god/before.py new file mode 100644 index 00000000..23d6e685 --- /dev/null +++ b/2026/god/before.py @@ -0,0 +1,196 @@ +import json +from pathlib import Path + +import joblib +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import accuracy_score, classification_report +from sklearn.model_selection import train_test_split + + +class ChurnExperiment: + """ + Deliberately bloated "before" example. + + This class is doing too much: + - input validation + - data loading + - cleaning + - feature engineering + - train/test split + - model training + - evaluation + - artifact saving + - reporting + """ + + def __init__( + self, + data_path: str = "data/churn.csv", + output_dir: str = "artifacts", + test_size: float = 0.2, + random_state: int = 42, + ) -> None: + self.data_path = Path(data_path) + self.output_dir = Path(output_dir) + self.test_size = test_size + self.random_state = random_state + self.model = RandomForestClassifier( + n_estimators=200, + max_depth=6, + random_state=random_state, + ) + + self.output_dir.mkdir(parents=True, exist_ok=True) + + def run(self) -> None: + print("Running churn experiment...\n") + + self._validate_inputs() + + df = self._load_data() + print("Raw sample:") + print(df.head(), "\n") + + df = self._clean_data(df) + X, y = self._prepare_features(df) + X_train, X_test, y_train, y_test = self._split_data(X, y) + + self._train_model(X_train, y_train) + metrics = self._evaluate_model(X_test, y_test) + self._save_artifacts(metrics, X.columns.tolist()) + self._print_summary(metrics, X.columns.tolist()) + + def _validate_inputs(self) -> None: + if not self.data_path.exists(): + raise FileNotFoundError(f"Dataset not found: {self.data_path}") + if self.data_path.suffix != ".csv": + raise ValueError("data_path must point to a CSV file") + if not 0 < self.test_size < 1: + raise ValueError("test_size must be between 0 and 1") + + def _load_data(self) -> pd.DataFrame: + return pd.read_csv(self.data_path) + + def _clean_data(self, df: pd.DataFrame) -> pd.DataFrame: + cleaned = df.copy() + + cleaned = cleaned.drop_duplicates(subset=["customer_id"]) + cleaned = cleaned.dropna(subset=["churn"]) + + cleaned["tenure"] = cleaned["tenure"].fillna(cleaned["tenure"].median()) + cleaned["monthly_charges"] = cleaned["monthly_charges"].fillna( + cleaned["monthly_charges"].median() + ) + + cleaned["support_tickets"] = cleaned["support_tickets"].clip(lower=0, upper=15) + + return cleaned + + def _prepare_features(self, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]: + prepared = df.copy() + + prepared["charges_per_ticket"] = prepared["monthly_charges"] / ( + prepared["support_tickets"] + 1 + ) + prepared["is_senior"] = (prepared["age"] >= 65).astype(int) + prepared["is_new_customer"] = (prepared["tenure"] < 6).astype(int) + + prepared = pd.get_dummies( + prepared, + columns=["contract_type", "internet_service", "payment_method"], + drop_first=True, + ) + + X = prepared.drop(columns=["customer_id", "churn"]) + y = prepared["churn"] + + return X, y + + def _split_data( + self, X: pd.DataFrame, y: pd.Series + ) -> tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: + return train_test_split( + X, + y, + test_size=self.test_size, + random_state=self.random_state, + stratify=y, + ) + + def _train_model(self, X_train: pd.DataFrame, y_train: pd.Series) -> None: + self.model.fit(X_train, y_train) + + def _evaluate_model( + self, X_test: pd.DataFrame, y_test: pd.Series + ) -> dict[str, object]: + predictions = self.model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + report = classification_report(y_test, predictions, output_dict=True) + + return { + "accuracy": accuracy, + "classification_report": report, + } + + def _save_artifacts( + self, metrics: dict[str, object], feature_names: list[str] + ) -> None: + model_path = self.output_dir / "churn_model.joblib" + metrics_path = self.output_dir / "metrics.json" + importances_path = self.output_dir / "feature_importances.csv" + + joblib.dump(self.model, model_path) + + with metrics_path.open("w", encoding="utf-8") as f: + json.dump(metrics, f, indent=2) + + importances = pd.DataFrame( + { + "feature": feature_names, + "importance": self.model.feature_importances_, + } + ).sort_values("importance", ascending=False) + + importances.to_csv(importances_path, index=False) + + def _print_summary( + self, metrics: dict[str, object], feature_names: list[str] + ) -> None: + print("Evaluation results:") + print(f"Accuracy: {metrics['accuracy']:.3f}\n") + + report = metrics["classification_report"] + print("Precision / Recall / F1:") + print( + f"Class 0 -> precision={report['0']['precision']:.3f}, " + f"recall={report['0']['recall']:.3f}, " + f"f1={report['0']['f1-score']:.3f}" + ) + print( + f"Class 1 -> precision={report['1']['precision']:.3f}, " + f"recall={report['1']['recall']:.3f}, " + f"f1={report['1']['f1-score']:.3f}\n" + ) + + importances = pd.DataFrame( + { + "feature": feature_names, + "importance": self.model.feature_importances_, + } + ).sort_values("importance", ascending=False) + + print("Top 10 feature importances:") + print(importances.head(10).to_string(index=False), "\n") + + print(f"Artifacts saved in: {self.output_dir.resolve()}") + + +if __name__ == "__main__": + experiment = ChurnExperiment( + data_path="data/churn.csv", + output_dir="artifacts", + test_size=0.25, + random_state=42, + ) + experiment.run() diff --git a/2026/god/config.py b/2026/god/config.py new file mode 100644 index 00000000..e42ee6f9 --- /dev/null +++ b/2026/god/config.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class TrainingConfig: + data_path: Path + output_dir: Path + test_size: float = 0.25 + random_state: int = 42 + + def __post_init__(self) -> None: + if not self.data_path.exists(): + raise FileNotFoundError(f"Dataset not found: {self.data_path}") + if self.data_path.suffix != ".csv": + raise ValueError("data_path must point to a CSV file") + if not 0 < self.test_size < 1: + raise ValueError("test_size must be between 0 and 1") diff --git a/2026/god/data_loading.py b/2026/god/data_loading.py new file mode 100644 index 00000000..bfa5af3f --- /dev/null +++ b/2026/god/data_loading.py @@ -0,0 +1,7 @@ +from pathlib import Path + +import pandas as pd + + +def load_dataset(path: Path) -> pd.DataFrame: + return pd.read_csv(path) diff --git a/2026/god/evaluation.py b/2026/god/evaluation.py new file mode 100644 index 00000000..9b914b97 --- /dev/null +++ b/2026/god/evaluation.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass + +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import accuracy_score, classification_report + + +@dataclass(frozen=True) +class EvaluationResult: + accuracy: float + classification_report: dict[str, object] + + def summary_lines(self) -> list[str]: + report = self.classification_report + return [ + f"Accuracy: {self.accuracy:.3f}", + ( + f"Class 0 -> precision={report['0']['precision']:.3f}, " + f"recall={report['0']['recall']:.3f}, " + f"f1={report['0']['f1-score']:.3f}" + ), + ( + f"Class 1 -> precision={report['1']['precision']:.3f}, " + f"recall={report['1']['recall']:.3f}, " + f"f1={report['1']['f1-score']:.3f}" + ), + ] + + +def evaluate_model( + model: RandomForestClassifier, + X_test: pd.DataFrame, + y_test: pd.Series, +) -> EvaluationResult: + predictions = model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + report = classification_report(y_test, predictions, output_dict=True) + + return EvaluationResult( + accuracy=accuracy, + classification_report=report, + ) diff --git a/2026/god/generate_data.py b/2026/god/generate_data.py new file mode 100644 index 00000000..1737c4a8 --- /dev/null +++ b/2026/god/generate_data.py @@ -0,0 +1,84 @@ +from pathlib import Path + +import numpy as np +import pandas as pd + + +def generate_synthetic_churn_data( + n_samples: int = 1500, + random_state: int = 42, +) -> pd.DataFrame: + rng = np.random.default_rng(random_state) + + age = rng.integers(18, 80, size=n_samples) + tenure = rng.integers(0, 72, size=n_samples).astype(float) + monthly_charges = rng.normal(70, 25, size=n_samples).clip(10, 200).astype(float) + support_tickets = rng.poisson(2.5, size=n_samples) + + contract_type = rng.choice( + ["monthly", "yearly", "two_year"], + size=n_samples, + p=[0.55, 0.30, 0.15], + ) + internet_service = rng.choice( + ["fiber", "dsl", "none"], + size=n_samples, + p=[0.50, 0.35, 0.15], + ) + payment_method = rng.choice( + ["credit_card", "bank_transfer", "paypal"], + size=n_samples, + p=[0.45, 0.35, 0.20], + ) + + # Create a somewhat realistic churn signal + logit = ( + -1.8 + + 0.015 * (monthly_charges - 70) + + 0.35 * support_tickets + - 0.03 * tenure + + 0.9 * (contract_type == "monthly") + + 0.35 * (internet_service == "fiber") + - 0.25 * (payment_method == "bank_transfer") + ) + + churn_probability = 1 / (1 + np.exp(-logit)) + churn = rng.binomial(1, churn_probability) + + df = pd.DataFrame( + { + "customer_id": [f"CUST-{i:05d}" for i in range(n_samples)], + "age": age, + "tenure": tenure, + "monthly_charges": monthly_charges, + "support_tickets": support_tickets, + "contract_type": contract_type, + "internet_service": internet_service, + "payment_method": payment_method, + "churn": churn, + } + ) + + # Add some missing values to simulate real data + missing_tenure_idx = rng.choice(n_samples, size=80, replace=False) + missing_charges_idx = rng.choice(n_samples, size=50, replace=False) + + df.loc[missing_tenure_idx, "tenure"] = np.nan + df.loc[missing_charges_idx, "monthly_charges"] = np.nan + + return df + + +def save_dataset(df: pd.DataFrame, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(path, index=False) + + +if __name__ == "__main__": + output_path = Path("data/churn.csv") + df = generate_synthetic_churn_data(n_samples=1500, random_state=42) + save_dataset(df, output_path) + + print(f"Saved dataset to {output_path.resolve()}") + print() + print(df.head()) diff --git a/2026/god/main.py b/2026/god/main.py new file mode 100644 index 00000000..991b8b19 --- /dev/null +++ b/2026/god/main.py @@ -0,0 +1,70 @@ +from artifacts import ( + compute_feature_importances, + save_feature_importances, + save_metrics, + save_model, +) +from config import TrainingConfig +from data_loading import load_dataset +from evaluation import evaluate_model +from preprocessing import clean_data, engineer_features, prepare_features_and_target +from training import split_data, train_model + + +def run_pipeline(config: TrainingConfig) -> None: + print("Running churn experiment...\n") + + df = load_dataset(config.data_path) + print("Raw sample:") + print(df.head(), "\n") + + cleaned = clean_data(df) + featured = engineer_features(cleaned) + X, y = prepare_features_and_target(featured) + + X_train, X_test, y_train, y_test = split_data( + X, + y, + test_size=config.test_size, + random_state=config.random_state, + ) + + model = train_model( + X_train, + y_train, + random_state=config.random_state, + ) + + result = evaluate_model(model, X_test, y_test) + importances = compute_feature_importances(model, X.columns.tolist()) + + model_path = config.output_dir / "churn_model.joblib" + metrics_path = config.output_dir / "metrics.json" + importances_path = config.output_dir / "feature_importances.csv" + + save_model(model, model_path) + save_metrics(result, metrics_path) + save_feature_importances(importances, importances_path) + + print("Evaluation results:") + for line in result.summary_lines(): + print(line) + + print("\nTop 10 feature importances:") + print(importances.head(10).to_string(index=False), "\n") + + print(f"Artifacts saved in: {config.output_dir.resolve()}") + + +def main() -> None: + config = TrainingConfig( + data_path="data/churn.csv", + output_dir="artifacts", + test_size=0.25, + random_state=42, + ) + run_pipeline(config) + + +if __name__ == "__main__": + main() diff --git a/2026/god/preprocessing.py b/2026/god/preprocessing.py new file mode 100644 index 00000000..fdf2accf --- /dev/null +++ b/2026/god/preprocessing.py @@ -0,0 +1,44 @@ +import pandas as pd + + +def clean_data(df: pd.DataFrame) -> pd.DataFrame: + cleaned = df.copy() + + cleaned = cleaned.drop_duplicates(subset=["customer_id"]) + cleaned = cleaned.dropna(subset=["churn"]) + + cleaned["tenure"] = cleaned["tenure"].fillna(cleaned["tenure"].median()) + cleaned["monthly_charges"] = cleaned["monthly_charges"].fillna( + cleaned["monthly_charges"].median() + ) + + cleaned["support_tickets"] = cleaned["support_tickets"].clip(lower=0, upper=15) + + return cleaned + + +def engineer_features(df: pd.DataFrame) -> pd.DataFrame: + featured = df.copy() + + featured["charges_per_ticket"] = featured["monthly_charges"] / ( + featured["support_tickets"] + 1 + ) + featured["is_senior"] = (featured["age"] >= 65).astype(int) + featured["is_new_customer"] = (featured["tenure"] < 6).astype(int) + + return featured + + +def prepare_features_and_target( + df: pd.DataFrame, +) -> tuple[pd.DataFrame, pd.Series]: + prepared = pd.get_dummies( + df, + columns=["contract_type", "internet_service", "payment_method"], + drop_first=True, + ) + + X = prepared.drop(columns=["customer_id", "churn"]) + y = prepared["churn"] + + return X, y diff --git a/2026/god/pyproject.toml b/2026/god/pyproject.toml new file mode 100644 index 00000000..fea8cbb6 --- /dev/null +++ b/2026/god/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "god" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ + "joblib>=1.5.3", + "numpy>=2.4.4", + "pandas>=3.0.2", + "scikit-learn>=1.8.0", +] diff --git a/2026/god/training.py b/2026/god/training.py new file mode 100644 index 00000000..ea1eb7a5 --- /dev/null +++ b/2026/god/training.py @@ -0,0 +1,32 @@ +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split + + +def split_data( + X: pd.DataFrame, + y: pd.Series, + test_size: float, + random_state: int, +) -> tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: + return train_test_split( + X, + y, + test_size=test_size, + random_state=random_state, + stratify=y, + ) + + +def train_model( + X_train: pd.DataFrame, + y_train: pd.Series, + random_state: int, +) -> RandomForestClassifier: + model = RandomForestClassifier( + n_estimators=200, + max_depth=6, + random_state=random_state, + ) + model.fit(X_train, y_train) + return model diff --git a/2026/god/uv.lock b/2026/god/uv.lock new file mode 100644 index 00000000..276e7a5c --- /dev/null +++ b/2026/god/uv.lock @@ -0,0 +1,190 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "god" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "scikit-learn" }, +] + +[package.metadata] +requires-dist = [ + { name = "joblib", specifier = ">=1.5.3" }, + { name = "numpy", specifier = ">=2.4.4" }, + { name = "pandas", specifier = ">=3.0.2" }, + { name = "scikit-learn", specifier = ">=1.8.0" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] From 9558aab766694d01d2c050d2d00348f0782866a0 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 23 Apr 2026 10:48:44 +0200 Subject: [PATCH 107/113] Simplified code example. Co-authored-by: Copilot --- 2026/god/after.py | 274 ++++++++++++++++++++++++++++++++++++++ 2026/god/artifacts.py | 41 ------ 2026/god/before.py | 56 +++----- 2026/god/config.py | 18 --- 2026/god/data_loading.py | 7 - 2026/god/evaluation.py | 42 ------ 2026/god/generate_data.py | 84 ------------ 2026/god/main.py | 70 ---------- 2026/god/preprocessing.py | 44 ------ 2026/god/training.py | 32 ----- 10 files changed, 296 insertions(+), 372 deletions(-) create mode 100644 2026/god/after.py delete mode 100644 2026/god/artifacts.py delete mode 100644 2026/god/config.py delete mode 100644 2026/god/data_loading.py delete mode 100644 2026/god/evaluation.py delete mode 100644 2026/god/generate_data.py delete mode 100644 2026/god/main.py delete mode 100644 2026/god/preprocessing.py delete mode 100644 2026/god/training.py diff --git a/2026/god/after.py b/2026/god/after.py new file mode 100644 index 00000000..7b9e6183 --- /dev/null +++ b/2026/god/after.py @@ -0,0 +1,274 @@ +import json +from dataclasses import dataclass +from pathlib import Path + +import joblib +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import accuracy_score, classification_report +from sklearn.model_selection import train_test_split + + +@dataclass(frozen=True) +class TrainingConfig: + data_path: Path + output_dir: Path + test_size: float = 0.2 + random_state: int = 42 + + def __post_init__(self) -> None: + if not self.data_path.exists(): + raise FileNotFoundError(f"Dataset not found: {self.data_path}") + if self.data_path.suffix != ".csv": + raise ValueError("data_path must point to a CSV file") + if not self.output_dir.parent.exists(): + raise FileNotFoundError( + f"Parent directory for output_dir does not exist: {self.output_dir.parent}" + ) + if not 0 < self.test_size < 1: + raise ValueError("test_size must be between 0 and 1") + if self.random_state < 0: + raise ValueError("random_state must be >= 0") + + def make_output_dir(self) -> None: + self.output_dir.mkdir(parents=True, exist_ok=True) + + @property + def model_path(self) -> Path: + return self.output_dir / "churn_model.joblib" + + @property + def metrics_path(self) -> Path: + return self.output_dir / "metrics.json" + + @property + def importances_path(self) -> Path: + return self.output_dir / "feature_importances.csv" + + +# --------------------------- +# Data loading +# --------------------------- + + +def load_data(path: Path) -> pd.DataFrame: + return pd.read_csv(path) + + +# --------------------------- +# Preprocessing +# --------------------------- + + +def clean_data(df: pd.DataFrame) -> pd.DataFrame: + cleaned = df.copy() + + cleaned = cleaned.drop_duplicates(subset=["customer_id"]) + cleaned = cleaned.dropna(subset=["churn"]) + + cleaned["tenure"] = cleaned["tenure"].fillna(cleaned["tenure"].median()) + cleaned["monthly_charges"] = cleaned["monthly_charges"].fillna( + cleaned["monthly_charges"].median() + ) + cleaned["support_tickets"] = cleaned["support_tickets"].clip(lower=0, upper=15) + + return cleaned + + +def engineer_features(df: pd.DataFrame) -> pd.DataFrame: + featured = df.copy() + + featured["charges_per_ticket"] = featured["monthly_charges"] / ( + featured["support_tickets"] + 1 + ) + featured["is_senior"] = (featured["age"] >= 65).astype(int) + featured["is_new_customer"] = (featured["tenure"] < 6).astype(int) + + return featured + + +def encode_categorical_features(df: pd.DataFrame) -> pd.DataFrame: + return pd.get_dummies( + df, + columns=["contract_type", "internet_service", "payment_method"], + drop_first=True, + ) + + +def split_features_and_target( + df: pd.DataFrame, +) -> tuple[pd.DataFrame, pd.Series]: + X = df.drop(columns=["customer_id", "churn"]) + y = df["churn"] + return X, y + + +# --------------------------- +# Training +# --------------------------- + + +def split_data( + X: pd.DataFrame, + y: pd.Series, + *, + test_size: float, + random_state: int, +) -> tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: + return train_test_split( + X, + y, + test_size=test_size, + random_state=random_state, + stratify=y, + ) + + +def build_model(random_state: int) -> RandomForestClassifier: + return RandomForestClassifier( + n_estimators=200, + max_depth=6, + random_state=random_state, + ) + + +def train_model( + model: RandomForestClassifier, + X_train: pd.DataFrame, + y_train: pd.Series, +) -> None: + model.fit(X_train, y_train) + + +# --------------------------- +# Evaluation +# --------------------------- + + +def evaluate_model( + model: RandomForestClassifier, + X_test: pd.DataFrame, + y_test: pd.Series, +) -> dict[str, object]: + predictions = model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + report = classification_report(y_test, predictions, output_dict=True) + + return { + "accuracy": accuracy, + "classification_report": report, + } + + +def compute_feature_importances( + model: RandomForestClassifier, + feature_names: list[str], +) -> pd.DataFrame: + return pd.DataFrame( + { + "feature": feature_names, + "importance": model.feature_importances_, + } + ).sort_values("importance", ascending=False) + + +# --------------------------- +# Persistence +# --------------------------- + + +def save_artifacts( + config: TrainingConfig, + model: RandomForestClassifier, + metrics: dict[str, object], + importances: pd.DataFrame, +) -> None: + config.make_output_dir() + + joblib.dump(model, config.model_path) + + with config.metrics_path.open("w", encoding="utf-8") as f: + json.dump(metrics, f, indent=2) + + importances.to_csv(config.importances_path, index=False) + + +# --------------------------- +# Reporting +# --------------------------- + + +def print_summary( + metrics: dict[str, object], + importances: pd.DataFrame, + output_dir: Path, +) -> None: + print("Evaluation results:") + print(f"Accuracy: {metrics['accuracy']:.3f}\n") + + report = metrics["classification_report"] + print("Precision / Recall / F1:") + print( + f"Class 0 -> precision={report['0']['precision']:.3f}, " + f"recall={report['0']['recall']:.3f}, " + f"f1={report['0']['f1-score']:.3f}" + ) + print( + f"Class 1 -> precision={report['1']['precision']:.3f}, " + f"recall={report['1']['recall']:.3f}, " + f"f1={report['1']['f1-score']:.3f}\n" + ) + + print("Top 10 feature importances:") + print(importances.head(10).to_string(index=False), "\n") + + print(f"Artifacts saved in: {output_dir.resolve()}") + + +# --------------------------- +# Orchestration +# --------------------------- + + +def run_experiment(config: TrainingConfig) -> None: + print("Running churn experiment...\n") + + df = load_data(config.data_path) + print("Raw sample:") + print(df.head(), "\n") + + df = clean_data(df) + df = engineer_features(df) + df = encode_categorical_features(df) + + X, y = split_features_and_target(df) + + X_train, X_test, y_train, y_test = split_data( + X, + y, + test_size=config.test_size, + random_state=config.random_state, + ) + + model = build_model(config.random_state) + train_model(model, X_train, y_train) + + metrics = evaluate_model(model, X_test, y_test) + importances = compute_feature_importances(model, X.columns.tolist()) + + save_artifacts(config, model, metrics, importances) + print_summary(metrics, importances, config.output_dir) + + +def main() -> None: + config = TrainingConfig( + data_path=Path("data/churn.csv"), + output_dir=Path("artifacts"), + test_size=0.25, + random_state=42, + ) + run_experiment(config) + + +if __name__ == "__main__": + main() diff --git a/2026/god/artifacts.py b/2026/god/artifacts.py deleted file mode 100644 index e7cf1d1e..00000000 --- a/2026/god/artifacts.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -from pathlib import Path - -import joblib -import pandas as pd -from evaluation import EvaluationResult -from sklearn.ensemble import RandomForestClassifier - - -def save_model(model: RandomForestClassifier, path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - joblib.dump(model, path) - - -def save_metrics(result: EvaluationResult, path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - - data = { - "accuracy": result.accuracy, - "classification_report": result.classification_report, - } - - with path.open("w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - - -def compute_feature_importances( - model: RandomForestClassifier, - feature_names: list[str], -) -> pd.DataFrame: - return pd.DataFrame( - { - "feature": feature_names, - "importance": model.feature_importances_, - } - ).sort_values("importance", ascending=False) - - -def save_feature_importances(importances: pd.DataFrame, path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - importances.to_csv(path, index=False) diff --git a/2026/god/before.py b/2026/god/before.py index 23d6e685..0e452165 100644 --- a/2026/god/before.py +++ b/2026/god/before.py @@ -9,21 +9,6 @@ class ChurnExperiment: - """ - Deliberately bloated "before" example. - - This class is doing too much: - - input validation - - data loading - - cleaning - - feature engineering - - train/test split - - model training - - evaluation - - artifact saving - - reporting - """ - def __init__( self, data_path: str = "data/churn.csv", @@ -41,18 +26,25 @@ def __init__( random_state=random_state, ) - self.output_dir.mkdir(parents=True, exist_ok=True) - def run(self) -> None: print("Running churn experiment...\n") - self._validate_inputs() + if not self.data_path.exists(): + raise FileNotFoundError(f"Dataset not found: {self.data_path}") + if self.data_path.suffix != ".csv": + raise ValueError("data_path must point to a CSV file") + if not self.output_dir.parent.exists(): + raise FileNotFoundError( + f"Parent directory for output_dir does not exist: {self.output_dir.parent}" + ) + if not 0 < self.test_size < 1: + raise ValueError("test_size must be between 0 and 1") + if self.random_state < 0: + raise ValueError("random_state must be >= 0") + + self.output_dir.mkdir(parents=True, exist_ok=True) df = self._load_data() - print("Raw sample:") - print(df.head(), "\n") - - df = self._clean_data(df) X, y = self._prepare_features(df) X_train, X_test, y_train, y_test = self._split_data(X, y) @@ -61,18 +53,11 @@ def run(self) -> None: self._save_artifacts(metrics, X.columns.tolist()) self._print_summary(metrics, X.columns.tolist()) - def _validate_inputs(self) -> None: - if not self.data_path.exists(): - raise FileNotFoundError(f"Dataset not found: {self.data_path}") - if self.data_path.suffix != ".csv": - raise ValueError("data_path must point to a CSV file") - if not 0 < self.test_size < 1: - raise ValueError("test_size must be between 0 and 1") - def _load_data(self) -> pd.DataFrame: - return pd.read_csv(self.data_path) + df = pd.read_csv(self.data_path) + print("Raw sample:") + print(df.head(), "\n") - def _clean_data(self, df: pd.DataFrame) -> pd.DataFrame: cleaned = df.copy() cleaned = cleaned.drop_duplicates(subset=["customer_id"]) @@ -82,7 +67,6 @@ def _clean_data(self, df: pd.DataFrame) -> pd.DataFrame: cleaned["monthly_charges"] = cleaned["monthly_charges"].fillna( cleaned["monthly_charges"].median() ) - cleaned["support_tickets"] = cleaned["support_tickets"].clip(lower=0, upper=15) return cleaned @@ -186,7 +170,7 @@ def _print_summary( print(f"Artifacts saved in: {self.output_dir.resolve()}") -if __name__ == "__main__": +def main() -> None: experiment = ChurnExperiment( data_path="data/churn.csv", output_dir="artifacts", @@ -194,3 +178,7 @@ def _print_summary( random_state=42, ) experiment.run() + + +if __name__ == "__main__": + main() diff --git a/2026/god/config.py b/2026/god/config.py deleted file mode 100644 index e42ee6f9..00000000 --- a/2026/god/config.py +++ /dev/null @@ -1,18 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - - -@dataclass(frozen=True) -class TrainingConfig: - data_path: Path - output_dir: Path - test_size: float = 0.25 - random_state: int = 42 - - def __post_init__(self) -> None: - if not self.data_path.exists(): - raise FileNotFoundError(f"Dataset not found: {self.data_path}") - if self.data_path.suffix != ".csv": - raise ValueError("data_path must point to a CSV file") - if not 0 < self.test_size < 1: - raise ValueError("test_size must be between 0 and 1") diff --git a/2026/god/data_loading.py b/2026/god/data_loading.py deleted file mode 100644 index bfa5af3f..00000000 --- a/2026/god/data_loading.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -import pandas as pd - - -def load_dataset(path: Path) -> pd.DataFrame: - return pd.read_csv(path) diff --git a/2026/god/evaluation.py b/2026/god/evaluation.py deleted file mode 100644 index 9b914b97..00000000 --- a/2026/god/evaluation.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass - -import pandas as pd -from sklearn.ensemble import RandomForestClassifier -from sklearn.metrics import accuracy_score, classification_report - - -@dataclass(frozen=True) -class EvaluationResult: - accuracy: float - classification_report: dict[str, object] - - def summary_lines(self) -> list[str]: - report = self.classification_report - return [ - f"Accuracy: {self.accuracy:.3f}", - ( - f"Class 0 -> precision={report['0']['precision']:.3f}, " - f"recall={report['0']['recall']:.3f}, " - f"f1={report['0']['f1-score']:.3f}" - ), - ( - f"Class 1 -> precision={report['1']['precision']:.3f}, " - f"recall={report['1']['recall']:.3f}, " - f"f1={report['1']['f1-score']:.3f}" - ), - ] - - -def evaluate_model( - model: RandomForestClassifier, - X_test: pd.DataFrame, - y_test: pd.Series, -) -> EvaluationResult: - predictions = model.predict(X_test) - accuracy = accuracy_score(y_test, predictions) - report = classification_report(y_test, predictions, output_dict=True) - - return EvaluationResult( - accuracy=accuracy, - classification_report=report, - ) diff --git a/2026/god/generate_data.py b/2026/god/generate_data.py deleted file mode 100644 index 1737c4a8..00000000 --- a/2026/god/generate_data.py +++ /dev/null @@ -1,84 +0,0 @@ -from pathlib import Path - -import numpy as np -import pandas as pd - - -def generate_synthetic_churn_data( - n_samples: int = 1500, - random_state: int = 42, -) -> pd.DataFrame: - rng = np.random.default_rng(random_state) - - age = rng.integers(18, 80, size=n_samples) - tenure = rng.integers(0, 72, size=n_samples).astype(float) - monthly_charges = rng.normal(70, 25, size=n_samples).clip(10, 200).astype(float) - support_tickets = rng.poisson(2.5, size=n_samples) - - contract_type = rng.choice( - ["monthly", "yearly", "two_year"], - size=n_samples, - p=[0.55, 0.30, 0.15], - ) - internet_service = rng.choice( - ["fiber", "dsl", "none"], - size=n_samples, - p=[0.50, 0.35, 0.15], - ) - payment_method = rng.choice( - ["credit_card", "bank_transfer", "paypal"], - size=n_samples, - p=[0.45, 0.35, 0.20], - ) - - # Create a somewhat realistic churn signal - logit = ( - -1.8 - + 0.015 * (monthly_charges - 70) - + 0.35 * support_tickets - - 0.03 * tenure - + 0.9 * (contract_type == "monthly") - + 0.35 * (internet_service == "fiber") - - 0.25 * (payment_method == "bank_transfer") - ) - - churn_probability = 1 / (1 + np.exp(-logit)) - churn = rng.binomial(1, churn_probability) - - df = pd.DataFrame( - { - "customer_id": [f"CUST-{i:05d}" for i in range(n_samples)], - "age": age, - "tenure": tenure, - "monthly_charges": monthly_charges, - "support_tickets": support_tickets, - "contract_type": contract_type, - "internet_service": internet_service, - "payment_method": payment_method, - "churn": churn, - } - ) - - # Add some missing values to simulate real data - missing_tenure_idx = rng.choice(n_samples, size=80, replace=False) - missing_charges_idx = rng.choice(n_samples, size=50, replace=False) - - df.loc[missing_tenure_idx, "tenure"] = np.nan - df.loc[missing_charges_idx, "monthly_charges"] = np.nan - - return df - - -def save_dataset(df: pd.DataFrame, path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - df.to_csv(path, index=False) - - -if __name__ == "__main__": - output_path = Path("data/churn.csv") - df = generate_synthetic_churn_data(n_samples=1500, random_state=42) - save_dataset(df, output_path) - - print(f"Saved dataset to {output_path.resolve()}") - print() - print(df.head()) diff --git a/2026/god/main.py b/2026/god/main.py deleted file mode 100644 index 991b8b19..00000000 --- a/2026/god/main.py +++ /dev/null @@ -1,70 +0,0 @@ -from artifacts import ( - compute_feature_importances, - save_feature_importances, - save_metrics, - save_model, -) -from config import TrainingConfig -from data_loading import load_dataset -from evaluation import evaluate_model -from preprocessing import clean_data, engineer_features, prepare_features_and_target -from training import split_data, train_model - - -def run_pipeline(config: TrainingConfig) -> None: - print("Running churn experiment...\n") - - df = load_dataset(config.data_path) - print("Raw sample:") - print(df.head(), "\n") - - cleaned = clean_data(df) - featured = engineer_features(cleaned) - X, y = prepare_features_and_target(featured) - - X_train, X_test, y_train, y_test = split_data( - X, - y, - test_size=config.test_size, - random_state=config.random_state, - ) - - model = train_model( - X_train, - y_train, - random_state=config.random_state, - ) - - result = evaluate_model(model, X_test, y_test) - importances = compute_feature_importances(model, X.columns.tolist()) - - model_path = config.output_dir / "churn_model.joblib" - metrics_path = config.output_dir / "metrics.json" - importances_path = config.output_dir / "feature_importances.csv" - - save_model(model, model_path) - save_metrics(result, metrics_path) - save_feature_importances(importances, importances_path) - - print("Evaluation results:") - for line in result.summary_lines(): - print(line) - - print("\nTop 10 feature importances:") - print(importances.head(10).to_string(index=False), "\n") - - print(f"Artifacts saved in: {config.output_dir.resolve()}") - - -def main() -> None: - config = TrainingConfig( - data_path="data/churn.csv", - output_dir="artifacts", - test_size=0.25, - random_state=42, - ) - run_pipeline(config) - - -if __name__ == "__main__": - main() diff --git a/2026/god/preprocessing.py b/2026/god/preprocessing.py deleted file mode 100644 index fdf2accf..00000000 --- a/2026/god/preprocessing.py +++ /dev/null @@ -1,44 +0,0 @@ -import pandas as pd - - -def clean_data(df: pd.DataFrame) -> pd.DataFrame: - cleaned = df.copy() - - cleaned = cleaned.drop_duplicates(subset=["customer_id"]) - cleaned = cleaned.dropna(subset=["churn"]) - - cleaned["tenure"] = cleaned["tenure"].fillna(cleaned["tenure"].median()) - cleaned["monthly_charges"] = cleaned["monthly_charges"].fillna( - cleaned["monthly_charges"].median() - ) - - cleaned["support_tickets"] = cleaned["support_tickets"].clip(lower=0, upper=15) - - return cleaned - - -def engineer_features(df: pd.DataFrame) -> pd.DataFrame: - featured = df.copy() - - featured["charges_per_ticket"] = featured["monthly_charges"] / ( - featured["support_tickets"] + 1 - ) - featured["is_senior"] = (featured["age"] >= 65).astype(int) - featured["is_new_customer"] = (featured["tenure"] < 6).astype(int) - - return featured - - -def prepare_features_and_target( - df: pd.DataFrame, -) -> tuple[pd.DataFrame, pd.Series]: - prepared = pd.get_dummies( - df, - columns=["contract_type", "internet_service", "payment_method"], - drop_first=True, - ) - - X = prepared.drop(columns=["customer_id", "churn"]) - y = prepared["churn"] - - return X, y diff --git a/2026/god/training.py b/2026/god/training.py deleted file mode 100644 index ea1eb7a5..00000000 --- a/2026/god/training.py +++ /dev/null @@ -1,32 +0,0 @@ -import pandas as pd -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split - - -def split_data( - X: pd.DataFrame, - y: pd.Series, - test_size: float, - random_state: int, -) -> tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: - return train_test_split( - X, - y, - test_size=test_size, - random_state=random_state, - stratify=y, - ) - - -def train_model( - X_train: pd.DataFrame, - y_train: pd.Series, - random_state: int, -) -> RandomForestClassifier: - model = RandomForestClassifier( - n_estimators=200, - max_depth=6, - random_state=random_state, - ) - model.fit(X_train, y_train) - return model From 84fe027f90d59a3a0828820351508ec4fe26d0f9 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 28 Apr 2026 14:47:16 +0200 Subject: [PATCH 108/113] Added data generation script. --- 2026/god/generate_data.py | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 2026/god/generate_data.py diff --git a/2026/god/generate_data.py b/2026/god/generate_data.py new file mode 100644 index 00000000..1737c4a8 --- /dev/null +++ b/2026/god/generate_data.py @@ -0,0 +1,84 @@ +from pathlib import Path + +import numpy as np +import pandas as pd + + +def generate_synthetic_churn_data( + n_samples: int = 1500, + random_state: int = 42, +) -> pd.DataFrame: + rng = np.random.default_rng(random_state) + + age = rng.integers(18, 80, size=n_samples) + tenure = rng.integers(0, 72, size=n_samples).astype(float) + monthly_charges = rng.normal(70, 25, size=n_samples).clip(10, 200).astype(float) + support_tickets = rng.poisson(2.5, size=n_samples) + + contract_type = rng.choice( + ["monthly", "yearly", "two_year"], + size=n_samples, + p=[0.55, 0.30, 0.15], + ) + internet_service = rng.choice( + ["fiber", "dsl", "none"], + size=n_samples, + p=[0.50, 0.35, 0.15], + ) + payment_method = rng.choice( + ["credit_card", "bank_transfer", "paypal"], + size=n_samples, + p=[0.45, 0.35, 0.20], + ) + + # Create a somewhat realistic churn signal + logit = ( + -1.8 + + 0.015 * (monthly_charges - 70) + + 0.35 * support_tickets + - 0.03 * tenure + + 0.9 * (contract_type == "monthly") + + 0.35 * (internet_service == "fiber") + - 0.25 * (payment_method == "bank_transfer") + ) + + churn_probability = 1 / (1 + np.exp(-logit)) + churn = rng.binomial(1, churn_probability) + + df = pd.DataFrame( + { + "customer_id": [f"CUST-{i:05d}" for i in range(n_samples)], + "age": age, + "tenure": tenure, + "monthly_charges": monthly_charges, + "support_tickets": support_tickets, + "contract_type": contract_type, + "internet_service": internet_service, + "payment_method": payment_method, + "churn": churn, + } + ) + + # Add some missing values to simulate real data + missing_tenure_idx = rng.choice(n_samples, size=80, replace=False) + missing_charges_idx = rng.choice(n_samples, size=50, replace=False) + + df.loc[missing_tenure_idx, "tenure"] = np.nan + df.loc[missing_charges_idx, "monthly_charges"] = np.nan + + return df + + +def save_dataset(df: pd.DataFrame, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(path, index=False) + + +if __name__ == "__main__": + output_path = Path("data/churn.csv") + df = generate_synthetic_churn_data(n_samples=1500, random_state=42) + save_dataset(df, output_path) + + print(f"Saved dataset to {output_path.resolve()}") + print() + print(df.head()) From de68821303c08a07da38cc7c6023d13257b5fd4e Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 28 Apr 2026 16:42:43 +0200 Subject: [PATCH 109/113] Added basic webhook examples. --- 2026/webhook/main.py | 95 ++++++++++++ 2026/webhook/pyproject.toml | 9 ++ 2026/webhook/uv.lock | 244 +++++++++++++++++++++++++++++++ 2026/webhook/webhook_receiver.py | 39 +++++ 4 files changed, 387 insertions(+) create mode 100644 2026/webhook/main.py create mode 100644 2026/webhook/pyproject.toml create mode 100644 2026/webhook/uv.lock create mode 100644 2026/webhook/webhook_receiver.py diff --git a/2026/webhook/main.py b/2026/webhook/main.py new file mode 100644 index 00000000..99a83cfb --- /dev/null +++ b/2026/webhook/main.py @@ -0,0 +1,95 @@ +from datetime import UTC, datetime +from uuid import UUID, uuid4 + +import httpx +from fastapi import FastAPI, HTTPException +from fastapi.responses import RedirectResponse +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + +links: dict[str, "ShortLink"] = {} +webhooks: dict[UUID, "Webhook"] = {} + + +class ShortLinkCreate(BaseModel): + target_url: HttpUrl + + +class ShortLink(BaseModel): + id: UUID + short_code: str + target_url: HttpUrl + clicks: int = 0 + + +class WebhookCreate(BaseModel): + url: HttpUrl + + +class Webhook(BaseModel): + id: UUID + url: HttpUrl + + +@app.post("/links") +def create_short_link(data: ShortLinkCreate) -> ShortLink: + short_code = uuid4().hex[:6] + + link = ShortLink( + id=uuid4(), + short_code=short_code, + target_url=data.target_url, + ) + + links[short_code] = link + return link + + +@app.get("/links") +def list_short_links() -> list[ShortLink]: + return list(links.values()) + + +@app.post("/webhooks") +def create_webhook(data: WebhookCreate) -> Webhook: + webhook = Webhook( + id=uuid4(), + url=data.url, + ) + + webhooks[webhook.id] = webhook + return webhook + + +@app.get("/webhooks") +def list_webhooks() -> list[Webhook]: + return list(webhooks.values()) + + +@app.get("/{short_code}") +def redirect_to_target(short_code: str) -> RedirectResponse: + link = links.get(short_code) + + if link is None: + raise HTTPException(status_code=404, detail="Short link not found") + + link.clicks += 1 + + payload = { + "type": "link.clicked", + "link_id": str(link.id), + "short_code": link.short_code, + "target_url": str(link.target_url), + "clicks": link.clicks, + "clicked_at": datetime.now(UTC).isoformat(), + } + + for webhook in webhooks.values(): + httpx.post( + str(webhook.url), + json=payload, + timeout=5, + ) + + return RedirectResponse(str(link.target_url)) diff --git a/2026/webhook/pyproject.toml b/2026/webhook/pyproject.toml new file mode 100644 index 00000000..15b2539c --- /dev/null +++ b/2026/webhook/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "webhook" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ + "fastapi>=0.136.1", + "httpx>=0.28.1", + "uvicorn>=0.46.0", +] diff --git a/2026/webhook/uv.lock b/2026/webhook/uv.lock new file mode 100644 index 00000000..7895d8f5 --- /dev/null +++ b/2026/webhook/uv.lock @@ -0,0 +1,244 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, +] + +[[package]] +name = "webhook" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "uvicorn", specifier = ">=0.46.0" }, +] diff --git a/2026/webhook/webhook_receiver.py b/2026/webhook/webhook_receiver.py new file mode 100644 index 00000000..f96ed56d --- /dev/null +++ b/2026/webhook/webhook_receiver.py @@ -0,0 +1,39 @@ +from datetime import UTC, datetime +from typing import Any + +from fastapi import FastAPI, Request +from pydantic import BaseModel + +app = FastAPI() + + +class ReceivedWebhook(BaseModel): + received_at: datetime + headers: dict[str, str] + payload: dict[str, Any] + + +received_webhooks: list[ReceivedWebhook] = [] + + +@app.post("/webhook") +async def receive_webhook(request: Request) -> dict[str, str]: + payload = await request.json() + + webhook = ReceivedWebhook( + received_at=datetime.now(UTC), + headers=dict(request.headers), + payload=payload, + ) + + received_webhooks.append(webhook) + + print("\nReceived webhook:") + print(webhook.model_dump_json(indent=2)) + + return {"status": "received"} + + +@app.get("/webhooks") +def list_received_webhooks() -> list[ReceivedWebhook]: + return received_webhooks From 9f8b617996be3e1528b46fb24a16698255cf5b32 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Thu, 30 Apr 2026 15:30:59 +0200 Subject: [PATCH 110/113] Worked on webhook example. --- 2026/webhook/README.md | 44 +++++++++++++++ 2026/webhook/v1/links.py | 53 ++++++++++++++++++ 2026/webhook/v1/main.py | 6 ++ 2026/webhook/{main.py => v2/links.py} | 56 ++++++------------ 2026/webhook/v2/main.py | 8 +++ 2026/webhook/v2/webhooks.py | 47 ++++++++++++++++ 2026/webhook/v3/events.py | 50 +++++++++++++++++ 2026/webhook/v3/links.py | 81 +++++++++++++++++++++++++++ 2026/webhook/v3/main.py | 15 +++++ 2026/webhook/v3/webhooks.py | 66 ++++++++++++++++++++++ 10 files changed, 388 insertions(+), 38 deletions(-) create mode 100644 2026/webhook/README.md create mode 100644 2026/webhook/v1/links.py create mode 100644 2026/webhook/v1/main.py rename 2026/webhook/{main.py => v2/links.py} (60%) create mode 100644 2026/webhook/v2/main.py create mode 100644 2026/webhook/v2/webhooks.py create mode 100644 2026/webhook/v3/events.py create mode 100644 2026/webhook/v3/links.py create mode 100644 2026/webhook/v3/main.py create mode 100644 2026/webhook/v3/webhooks.py diff --git a/2026/webhook/README.md b/2026/webhook/README.md new file mode 100644 index 00000000..509b1584 --- /dev/null +++ b/2026/webhook/README.md @@ -0,0 +1,44 @@ +Start the main API + +``` +uvicorn main:app --reload --port 8000 +``` + +Start the webhook receiver + +``` +uvicorn webhook_receiver:app --reload --port 8001 +``` + +## Links + +Create a short link + +curl -X POST http://localhost:8000/links \ + -H "Content-Type: application/json" \ + -d '{"target_url": "https://www.arjancodes.com"}' + +### Webhook + +Create a simple webhook: + +``` +curl -X POST http://localhost:8000/webhooks \ + -H "Content-Type: application/json" \ + -d '{ + "url": "http://localhost:8001/webhook" + }' +``` + +### Events version + +Create a webhook: + +``` +curl -X POST http://localhost:8000/webhooks \ + -H "Content-Type: application/json" \ + -d '{ + "url": "http://localhost:8001/webhook", + "events": ["link.clicked"] + }' +``` \ No newline at end of file diff --git a/2026/webhook/v1/links.py b/2026/webhook/v1/links.py new file mode 100644 index 00000000..0674339c --- /dev/null +++ b/2026/webhook/v1/links.py @@ -0,0 +1,53 @@ +from uuid import UUID, uuid4 + +from fastapi import APIRouter, HTTPException +from fastapi.responses import RedirectResponse +from pydantic import BaseModel, HttpUrl + +router = APIRouter() + + +class ShortLinkCreate(BaseModel): + target_url: HttpUrl + + +class ShortLink(BaseModel): + id: UUID + short_code: str + target_url: HttpUrl + clicks: int = 0 + + +links: dict[str, ShortLink] = {} + + +@router.post("/links") +def create_short_link(data: ShortLinkCreate) -> ShortLink: + short_code = uuid4().hex[:6] + + link = ShortLink( + id=uuid4(), + short_code=short_code, + target_url=data.target_url, + ) + + links[short_code] = link + + return link + + +@router.get("/links") +def list_short_links() -> list[ShortLink]: + return list(links.values()) + + +@router.get("/{short_code}") +def redirect_to_target(short_code: str) -> RedirectResponse: + link = links.get(short_code) + + if link is None: + raise HTTPException(status_code=404, detail="Short link not found") + + link.clicks += 1 + + return RedirectResponse(str(link.target_url)) diff --git a/2026/webhook/v1/main.py b/2026/webhook/v1/main.py new file mode 100644 index 00000000..5c0f5b5d --- /dev/null +++ b/2026/webhook/v1/main.py @@ -0,0 +1,6 @@ +import links +from fastapi import FastAPI + +app = FastAPI() + +app.include_router(links.router) diff --git a/2026/webhook/main.py b/2026/webhook/v2/links.py similarity index 60% rename from 2026/webhook/main.py rename to 2026/webhook/v2/links.py index 99a83cfb..17a02959 100644 --- a/2026/webhook/main.py +++ b/2026/webhook/v2/links.py @@ -1,15 +1,12 @@ from datetime import UTC, datetime from uuid import UUID, uuid4 -import httpx -from fastapi import FastAPI, HTTPException +from fastapi import APIRouter, HTTPException from fastapi.responses import RedirectResponse from pydantic import BaseModel, HttpUrl +from webhooks import send_webhooks -app = FastAPI() - -links: dict[str, "ShortLink"] = {} -webhooks: dict[UUID, "Webhook"] = {} +router = APIRouter() class ShortLinkCreate(BaseModel): @@ -23,16 +20,10 @@ class ShortLink(BaseModel): clicks: int = 0 -class WebhookCreate(BaseModel): - url: HttpUrl - +links: dict[str, ShortLink] = {} -class Webhook(BaseModel): - id: UUID - url: HttpUrl - -@app.post("/links") +@router.post("/links") def create_short_link(data: ShortLinkCreate) -> ShortLink: short_code = uuid4().hex[:6] @@ -43,31 +34,25 @@ def create_short_link(data: ShortLinkCreate) -> ShortLink: ) links[short_code] = link - return link - - -@app.get("/links") -def list_short_links() -> list[ShortLink]: - return list(links.values()) + payload = { + "type": "link.created", + "link_id": str(link.id), + "short_code": link.short_code, + "target_url": str(link.target_url), + } -@app.post("/webhooks") -def create_webhook(data: WebhookCreate) -> Webhook: - webhook = Webhook( - id=uuid4(), - url=data.url, - ) + send_webhooks(payload) - webhooks[webhook.id] = webhook - return webhook + return link -@app.get("/webhooks") -def list_webhooks() -> list[Webhook]: - return list(webhooks.values()) +@router.get("/links") +def list_short_links() -> list[ShortLink]: + return list(links.values()) -@app.get("/{short_code}") +@router.get("/{short_code}") def redirect_to_target(short_code: str) -> RedirectResponse: link = links.get(short_code) @@ -85,11 +70,6 @@ def redirect_to_target(short_code: str) -> RedirectResponse: "clicked_at": datetime.now(UTC).isoformat(), } - for webhook in webhooks.values(): - httpx.post( - str(webhook.url), - json=payload, - timeout=5, - ) + send_webhooks(payload) return RedirectResponse(str(link.target_url)) diff --git a/2026/webhook/v2/main.py b/2026/webhook/v2/main.py new file mode 100644 index 00000000..49ea0b7e --- /dev/null +++ b/2026/webhook/v2/main.py @@ -0,0 +1,8 @@ +import links +import webhooks +from fastapi import FastAPI + +app = FastAPI() + +app.include_router(links.router) +app.include_router(webhooks.router) diff --git a/2026/webhook/v2/webhooks.py b/2026/webhook/v2/webhooks.py new file mode 100644 index 00000000..992b2121 --- /dev/null +++ b/2026/webhook/v2/webhooks.py @@ -0,0 +1,47 @@ +from typing import Any +from uuid import UUID, uuid4 + +import httpx +from fastapi import APIRouter +from pydantic import BaseModel, HttpUrl + + +class Webhook(BaseModel): + id: UUID + url: HttpUrl + + +class WebhookCreate(BaseModel): + url: HttpUrl + + +router = APIRouter() + +webhooks: dict[UUID, Webhook] = {} + + +def send_webhooks(data: dict[str, Any]) -> None: + for webhook in webhooks.values(): + deliver_webhook(webhook, data) + + +def deliver_webhook(webhook: Webhook, data: dict[str, Any]) -> None: + httpx.post( + str(webhook.url), + json=data, + timeout=5, + ) + + +@router.post("/webhooks") +def create_webhook(data: WebhookCreate) -> Webhook: + webhook = Webhook(id=uuid4(), url=data.url) + + webhooks[webhook.id] = webhook + + return webhook + + +@router.get("/webhooks") +def list_webhooks() -> list[Webhook]: + return list(webhooks.values()) diff --git a/2026/webhook/v3/events.py b/2026/webhook/v3/events.py new file mode 100644 index 00000000..859f471f --- /dev/null +++ b/2026/webhook/v3/events.py @@ -0,0 +1,50 @@ +from datetime import UTC, datetime +from enum import StrEnum +from typing import Any, Callable +from uuid import UUID, uuid4 + +from pydantic import BaseModel + + +class EventType(StrEnum): + LINK_CREATED = "link.created" + LINK_CLICKED = "link.clicked" + + +class Event(BaseModel): + id: UUID + type: EventType + occurred_at: datetime + data: dict[str, Any] + + +EventListener = Callable[[Event], None] + + +class EventBus: + def __init__(self) -> None: + self._listeners: dict[EventType, list[EventListener]] = {} + + def subscribe( + self, + event_type: EventType, + listener: EventListener, + ) -> None: + self._listeners.setdefault(event_type, []).append(listener) + + def publish( + self, + event_type: EventType, + data: dict[str, Any], + ) -> Event: + event = Event( + id=uuid4(), + type=event_type, + occurred_at=datetime.now(UTC), + data=data, + ) + + for listener in self._listeners.get(event.type, []): + listener(event) + + return event diff --git a/2026/webhook/v3/links.py b/2026/webhook/v3/links.py new file mode 100644 index 00000000..058ae19b --- /dev/null +++ b/2026/webhook/v3/links.py @@ -0,0 +1,81 @@ +from uuid import UUID, uuid4 + +from events import EventBus, EventType +from fastapi import APIRouter, HTTPException +from fastapi.responses import RedirectResponse +from pydantic import BaseModel, HttpUrl + +router = APIRouter() + + +class ShortLinkCreate(BaseModel): + target_url: HttpUrl + + +class ShortLink(BaseModel): + id: UUID + short_code: str + target_url: HttpUrl + clicks: int = 0 + + +links: dict[str, ShortLink] = {} +event_bus: EventBus | None = None + + +def configure(bus: EventBus) -> None: + global event_bus + event_bus = bus + + +@router.post("/links") +def create_short_link(data: ShortLinkCreate) -> ShortLink: + short_code = uuid4().hex[:6] + + link = ShortLink( + id=uuid4(), + short_code=short_code, + target_url=data.target_url, + ) + + links[short_code] = link + + if event_bus is not None: + event_bus.publish( + EventType.LINK_CREATED, + { + "link_id": str(link.id), + "short_code": link.short_code, + "target_url": str(link.target_url), + }, + ) + + return link + + +@router.get("/links") +def list_short_links() -> list[ShortLink]: + return list(links.values()) + + +@router.get("/{short_code}") +def redirect_to_target(short_code: str) -> RedirectResponse: + link = links.get(short_code) + + if link is None: + raise HTTPException(status_code=404, detail="Short link not found") + + link.clicks += 1 + + if event_bus is not None: + event_bus.publish( + EventType.LINK_CLICKED, + { + "link_id": str(link.id), + "short_code": link.short_code, + "target_url": str(link.target_url), + "clicks": link.clicks, + }, + ) + + return RedirectResponse(str(link.target_url)) diff --git a/2026/webhook/v3/main.py b/2026/webhook/v3/main.py new file mode 100644 index 00000000..7d66dc96 --- /dev/null +++ b/2026/webhook/v3/main.py @@ -0,0 +1,15 @@ +import links +import webhooks +from events import EventBus +from fastapi import FastAPI + +app = FastAPI() + + +event_bus = EventBus() + +links.configure(event_bus) +webhooks.configure(event_bus) + +app.include_router(links.router) +app.include_router(webhooks.router) diff --git a/2026/webhook/v3/webhooks.py b/2026/webhook/v3/webhooks.py new file mode 100644 index 00000000..08078efe --- /dev/null +++ b/2026/webhook/v3/webhooks.py @@ -0,0 +1,66 @@ +from uuid import UUID, uuid4 + +import httpx +from events import Event, EventBus, EventType +from fastapi import APIRouter +from pydantic import BaseModel, HttpUrl + + +class Webhook(BaseModel): + id: UUID + url: HttpUrl + events: list[EventType] + + +class WebhookCreate(BaseModel): + url: HttpUrl + events: list[EventType] + + +router = APIRouter() + +webhooks: dict[UUID, Webhook] = {} +event_bus: EventBus | None = None + + +def configure(bus: EventBus) -> None: + global event_bus + event_bus = bus + + +def deliver_webhook(webhook: Webhook, event: Event) -> None: + httpx.post( + str(webhook.url), + json=event.model_dump(mode="json"), + timeout=5, + ) + + +def attach_webhook_listener(webhook: Webhook) -> None: + if event_bus is None: + return + + for event_type in webhook.events: + event_bus.subscribe( + event_type, + lambda event, webhook=webhook: deliver_webhook(webhook, event), + ) + + +@router.post("/webhooks") +def create_webhook(data: WebhookCreate) -> Webhook: + webhook = Webhook( + id=uuid4(), + url=data.url, + events=data.events, + ) + + webhooks[webhook.id] = webhook + attach_webhook_listener(webhook) + + return webhook + + +@router.get("/webhooks") +def list_webhooks() -> list[Webhook]: + return list(webhooks.values()) From 7b1dc167cb722b49d87a0443d6f49827f4c1d554 Mon Sep 17 00:00:00 2001 From: Arjan Egges Date: Tue, 28 Apr 2026 15:23:40 +0200 Subject: [PATCH 111/113] Added code example. --- 2026/nested/after.py | 132 +++++++++++++++++++++++++++++++++++++ 2026/nested/before.py | 110 +++++++++++++++++++++++++++++++ 2026/nested/pyproject.toml | 6 ++ 3 files changed, 248 insertions(+) create mode 100644 2026/nested/after.py create mode 100644 2026/nested/before.py create mode 100644 2026/nested/pyproject.toml diff --git a/2026/nested/after.py b/2026/nested/after.py new file mode 100644 index 00000000..8ecaa5ab --- /dev/null +++ b/2026/nested/after.py @@ -0,0 +1,132 @@ +from dataclasses import dataclass +from decimal import Decimal + + +@dataclass(frozen=True) +class Customer: + id: int + name: str + tier: str + + +@dataclass(frozen=True) +class OrderItem: + price: Decimal + quantity: int + + +@dataclass(frozen=True) +class Order: + customer_id: int + status: str + items: list[OrderItem] + + @property + def total(self) -> Decimal: + return sum((item.price * item.quantity for item in self.items), Decimal("0")) + + +@dataclass(frozen=True) +class CustomerSummary: + customer_name: str + paid_orders: int + total_spent: Decimal + + +def group_orders_by_customer( + orders: list[Order], +) -> dict[int, list[Order]]: + orders_by_customer: dict[int, list[Order]] = {} + + for order in orders: + orders_by_customer.setdefault(order.customer_id, []).append(order) + + return orders_by_customer + + +def paid_orders(orders: list[Order]) -> list[Order]: + return [order for order in orders if order.status == "paid"] + + +def apply_discount(customer: Customer, total: Decimal) -> Decimal: + if customer.tier == "premium" and total > Decimal("100"): + return total * Decimal("0.9") + + return total + + +def build_customer_summary( + customer: Customer, + orders: list[Order], +) -> CustomerSummary: + totals = [apply_discount(customer, order.total) for order in paid_orders(orders)] + + return CustomerSummary( + customer_name=customer.name, + paid_orders=len(totals), + total_spent=sum(totals, Decimal("0")), + ) + + +def generate_customer_report( + customers: list[Customer], + orders: list[Order], +) -> list[CustomerSummary]: + orders_by_customer = group_orders_by_customer(orders) + summaries: list[CustomerSummary] = [] + + for customer in customers: + customer_orders = orders_by_customer.get(customer.id, []) + + if not customer_orders: + continue + + summary = build_customer_summary(customer, customer_orders) + summaries.append(summary) + + return summaries + + +def main() -> None: + customers = [ + Customer(id=1, name="Alice", tier="premium"), + Customer(id=2, name="Bob", tier="standard"), + ] + + orders = [ + Order( + customer_id=1, + status="paid", + items=[ + OrderItem(price=Decimal("30"), quantity=2), + OrderItem(price=Decimal("50"), quantity=1), + ], + ), + Order( + customer_id=1, + status="pending", + items=[ + OrderItem(price=Decimal("20"), quantity=1), + ], + ), + Order( + customer_id=2, + status="paid", + items=[ + OrderItem(price=Decimal("40"), quantity=1), + ], + ), + ] + + report = generate_customer_report(customers, orders) + + for summary in report: + print( + f"{summary.customer_name}: " + f"{summary.paid_orders} orders, " + f"total spent = {summary.total_spent}" + ) + + +if __name__ == "__main__": + main() diff --git a/2026/nested/before.py b/2026/nested/before.py new file mode 100644 index 00000000..4914ba1e --- /dev/null +++ b/2026/nested/before.py @@ -0,0 +1,110 @@ +from dataclasses import dataclass +from decimal import Decimal + + +@dataclass(frozen=True) +class Customer: + id: int + name: str + tier: str + + +@dataclass(frozen=True) +class OrderItem: + price: Decimal + quantity: int + + +@dataclass(frozen=True) +class Order: + customer_id: int + status: str + items: list[OrderItem] + + +@dataclass(frozen=True) +class CustomerSummary: + customer_name: str + paid_orders: int + total_spent: Decimal + + +def generate_customer_report( + customers: list[Customer], + orders: list[Order], +) -> list[CustomerSummary]: + report: list[CustomerSummary] = [] + + for customer in customers: + total_spent = Decimal("0") + paid_order_count = 0 + + for order in orders: + if order.customer_id == customer.id: + if order.status == "paid": + order_total = Decimal("0") + + for item in order.items: + order_total += item.price * item.quantity + + if customer.tier == "premium" and order_total > Decimal("100"): + order_total *= Decimal("0.9") + + total_spent += order_total + paid_order_count += 1 + + if paid_order_count > 0: + report.append( + CustomerSummary( + customer_name=customer.name, + paid_orders=paid_order_count, + total_spent=total_spent, + ) + ) + + return report + + +def main() -> None: + customers = [ + Customer(id=1, name="Alice", tier="premium"), + Customer(id=2, name="Bob", tier="standard"), + ] + + orders = [ + Order( + customer_id=1, + status="paid", + items=[ + OrderItem(price=Decimal("30"), quantity=2), + OrderItem(price=Decimal("50"), quantity=1), + ], + ), + Order( + customer_id=1, + status="pending", + items=[ + OrderItem(price=Decimal("20"), quantity=1), + ], + ), + Order( + customer_id=2, + status="paid", + items=[ + OrderItem(price=Decimal("40"), quantity=1), + ], + ), + ] + + report = generate_customer_report(customers, orders) + + for summary in report: + print( + f"{summary.customer_name}: " + f"{summary.paid_orders} orders, " + f"total spent = {summary.total_spent}" + ) + + +if __name__ == "__main__": + main() diff --git a/2026/nested/pyproject.toml b/2026/nested/pyproject.toml new file mode 100644 index 00000000..03e7bfae --- /dev/null +++ b/2026/nested/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "nested" +version = "0.0.1" +requires-python = ">=3.14" +dependencies = [ +] From 4c5cec16d822cc060747bad5939b6a06fe5784f7 Mon Sep 17 00:00:00 2001 From: toddmath Date: Wed, 27 May 2026 13:32:34 -0700 Subject: [PATCH 112/113] Improve code quality, documentation, and type safety Enables basic type checking in VS Code and adds comprehensive docstrings to classes and methods to improve maintainability and clarity. Refines error handling by catching more specific exceptions and avoids shadowing built-in names. Also simplifies factory logic and updates project configuration for better consistency. --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f0bed7bd..acc4cc39 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,5 @@ // "python.testing.unittestEnabled": false, "python.testing.cwd": "${workspaceFolder}/tests", "python.defaultInterpreterPath": "${workspaceFolder}/2025/typescript/lokalise/.venv/bin/python", - // "python.analysis.typeCheckingMode": "off" + "python.analysis.typeCheckingMode": "basic" } From 1f2fa894ff1d2bed51c58da7f0f012aa4f635497 Mon Sep 17 00:00:00 2001 From: Todd Matheson <8497474+toddmath@users.noreply.github.com> Date: Wed, 27 May 2026 15:05:28 -0700 Subject: [PATCH 113/113] Delete Ruff linting and formatting workflow Remove Python linting and formatting workflow using Ruff. --- .github/workflows/format.yaml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index 47c901c3..8b137891 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -1,30 +1 @@ -name: Python Linting and formatting with Ruff -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT }} - - - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install Ruff - run: | - python -m pip install --upgrade pip - pip install ruff - - run: | - ruff format - - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: 'Code formatted with Ruff'