From 85c5ecb349a2c43c63a90a071697151086b6f24c Mon Sep 17 00:00:00 2001 From: Eugene Sim Date: Thu, 23 Apr 2026 16:29:54 +0800 Subject: [PATCH 1/2] Build a real-data Python learning pipeline with World Bank integration. Expand the project into a testable curriculum package by adding World Bank ingestion, cache-aware capstone orchestration, API and CLI workflows, extensive tests, and a fully reorganized multi-section documentation system. --- .github/workflows/ci.yml | 28 + Binary Search.ipynb | 377 +++++--- Find the month.ipynb | 169 ++-- Guess the number.ipynb | 216 +++-- Hangman.ipynb | 573 ++++++----- Minesweeper.ipynb | 890 +++++++++++------- README.md | 160 +++- Rock Paper Scissors.ipynb | 220 +++-- Simple Calculator.ipynb | 195 ++-- compose.py | 2 +- compose_template.py | 30 +- docs/README.md | 35 + .../curriculum/modules/core_and_algorithms.md | 35 + docs/curriculum/modules/systems_and_data.md | 55 ++ docs/curriculum/overview.md | 25 + docs/getting-started/quickstart.md | 46 + docs/migration/script-modernization.md | 39 + docs/pipelines/world-bank/api-contracts.md | 63 ++ docs/pipelines/world-bank/operations.md | 42 + docs/pipelines/world-bank/overview.md | 33 + docs/python_expansion_curriculum.md | 11 + docs/script_modernization_guide.md | 11 + docs/testing/verification.md | 27 + docs/world_bank_data_pipeline.md | 11 + graph_template.py | 36 +- lyrics.py | 18 +- madlib.ipynb | 258 +++-- requirements-dev.txt | 3 + requirements.txt | 8 + src/mini_projects/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 224 bytes .../__pycache__/cli.cpython-313.pyc | Bin 0 -> 4226 bytes src/mini_projects/cli.py | 126 +++ src/mini_projects/features/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 236 bytes .../features/advanced_python/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 257 bytes .../advanced_python/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 277 bytes .../__pycache__/patterns.cpython-313.pyc | Bin 0 -> 3495 bytes .../advanced_python/application/patterns.py | 68 ++ .../features/async_concurrency/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 258 bytes .../async_concurrency/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 279 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 0 -> 3634 bytes .../async_concurrency/application/pipeline.py | 64 ++ .../features/backend_api/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 242 bytes .../features/backend_api/domain/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 258 bytes .../domain/__pycache__/models.cpython-313.pyc | Bin 0 -> 667 bytes .../features/backend_api/domain/models.py | 10 + .../backend_api/infrastructure/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 276 bytes .../__pycache__/repository.cpython-313.pyc | Bin 0 -> 2625 bytes .../backend_api/infrastructure/repository.py | 41 + .../backend_api/presentation/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 275 bytes .../__pycache__/app.cpython-313.pyc | Bin 0 -> 4994 bytes .../features/backend_api/presentation/app.py | 76 ++ .../features/big_data/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 245 bytes .../features/big_data/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 264 bytes .../chunked_processing.cpython-313.pyc | Bin 0 -> 1180 bytes .../application/chunked_processing.py | 26 + .../features/capstone/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 247 bytes .../features/capstone/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 264 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 0 -> 5319 bytes .../features/capstone/application/pipeline.py | 159 ++++ .../capstone/infrastructure/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 283 bytes .../__pycache__/cache_store.cpython-313.pyc | Bin 0 -> 2054 bytes .../world_bank_client.cpython-313.pyc | Bin 0 -> 6572 bytes .../capstone/infrastructure/cache_store.py | 27 + .../infrastructure/world_bank_client.py | 125 +++ .../features/dashboard/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 247 bytes .../dashboard/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 266 bytes .../__pycache__/dashboard.cpython-313.pyc | Bin 0 -> 1378 bytes .../dashboard/application/dashboard.py | 28 + .../features/data_stack/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 243 bytes .../data_stack/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 264 bytes .../__pycache__/analysis.cpython-313.pyc | Bin 0 -> 5058 bytes .../data_stack/application/analysis.py | 96 ++ src/mini_projects/features/dsa/__init__.py | 1 + .../dsa/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 253 bytes .../features/dsa/application/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 254 bytes .../__pycache__/algorithms.cpython-313.pyc | Bin 0 -> 5822 bytes .../features/dsa/application/algorithms.py | 109 +++ .../features/tictactoe/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 238 bytes .../tictactoe/application/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 259 bytes .../__pycache__/ai.cpython-313.pyc | Bin 0 -> 5108 bytes .../features/tictactoe/application/ai.py | 113 +++ .../features/tictactoe/application/game.py | 36 + .../features/tictactoe/domain/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 247 bytes .../domain/__pycache__/board.cpython-313.pyc | Bin 0 -> 4356 bytes .../features/tictactoe/domain/board.py | 60 ++ .../tictactoe/presentation/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 258 bytes .../__pycache__/cli.cpython-313.pyc | Bin 0 -> 4463 bytes .../features/tictactoe/presentation/cli.py | 79 ++ sudoku.ipynb | 437 ++++++--- .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 693 bytes .../test_cli.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 9033 bytes tests/conftest.py | 10 + ...test_patterns.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 4480 bytes .../features/advanced_python/test_patterns.py | 30 + ...test_pipeline.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 4678 bytes .../async_concurrency/test_pipeline.py | 37 + .../test_app.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 9879 bytes tests/features/backend_api/test_app.py | 55 ++ ...ed_processing.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 3934 bytes .../big_data/test_chunked_processing.py | 14 + ...tone_pipeline.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 2573 bytes ...d_bank_client.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 5667 bytes ..._bank_refresh.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 5045 bytes .../capstone/test_capstone_pipeline.py | 30 + .../capstone/test_world_bank_client.py | 88 ++ .../capstone/test_world_bank_refresh.py | 68 ++ ...test_analysis.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 4225 bytes tests/features/data_stack/test_analysis.py | 55 ++ ...st_algorithms.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 6252 bytes tests/features/dsa/test_algorithms.py | 34 + .../test_ai.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 6300 bytes .../test_board.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 6388 bytes tests/features/tictactoe/test_ai.py | 28 + tests/features/tictactoe/test_board.py | 34 + tests/test_cli.py | 47 + transform_empty.py | 51 +- 140 files changed, 4645 insertions(+), 1263 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/README.md create mode 100644 docs/curriculum/modules/core_and_algorithms.md create mode 100644 docs/curriculum/modules/systems_and_data.md create mode 100644 docs/curriculum/overview.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/migration/script-modernization.md create mode 100644 docs/pipelines/world-bank/api-contracts.md create mode 100644 docs/pipelines/world-bank/operations.md create mode 100644 docs/pipelines/world-bank/overview.md create mode 100644 docs/python_expansion_curriculum.md create mode 100644 docs/script_modernization_guide.md create mode 100644 docs/testing/verification.md create mode 100644 docs/world_bank_data_pipeline.md create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 src/mini_projects/__init__.py create mode 100644 src/mini_projects/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/__pycache__/cli.cpython-313.pyc create mode 100644 src/mini_projects/cli.py create mode 100644 src/mini_projects/features/__init__.py create mode 100644 src/mini_projects/features/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/advanced_python/__init__.py create mode 100644 src/mini_projects/features/advanced_python/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/advanced_python/application/__init__.py create mode 100644 src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/advanced_python/application/__pycache__/patterns.cpython-313.pyc create mode 100644 src/mini_projects/features/advanced_python/application/patterns.py create mode 100644 src/mini_projects/features/async_concurrency/__init__.py create mode 100644 src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/async_concurrency/application/__init__.py create mode 100644 src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/async_concurrency/application/__pycache__/pipeline.cpython-313.pyc create mode 100644 src/mini_projects/features/async_concurrency/application/pipeline.py create mode 100644 src/mini_projects/features/backend_api/__init__.py create mode 100644 src/mini_projects/features/backend_api/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/domain/__init__.py create mode 100644 src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/domain/models.py create mode 100644 src/mini_projects/features/backend_api/infrastructure/__init__.py create mode 100644 src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/infrastructure/repository.py create mode 100644 src/mini_projects/features/backend_api/presentation/__init__.py create mode 100644 src/mini_projects/features/backend_api/presentation/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc create mode 100644 src/mini_projects/features/backend_api/presentation/app.py create mode 100644 src/mini_projects/features/big_data/__init__.py create mode 100644 src/mini_projects/features/big_data/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/big_data/application/__init__.py create mode 100644 src/mini_projects/features/big_data/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/big_data/application/__pycache__/chunked_processing.cpython-313.pyc create mode 100644 src/mini_projects/features/big_data/application/chunked_processing.py create mode 100644 src/mini_projects/features/capstone/__init__.py create mode 100644 src/mini_projects/features/capstone/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/application/__init__.py create mode 100644 src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/application/__pycache__/pipeline.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/application/pipeline.py create mode 100644 src/mini_projects/features/capstone/infrastructure/__init__.py create mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/world_bank_client.cpython-313.pyc create mode 100644 src/mini_projects/features/capstone/infrastructure/cache_store.py create mode 100644 src/mini_projects/features/capstone/infrastructure/world_bank_client.py create mode 100644 src/mini_projects/features/dashboard/__init__.py create mode 100644 src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/dashboard/application/__init__.py create mode 100644 src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc create mode 100644 src/mini_projects/features/dashboard/application/dashboard.py create mode 100644 src/mini_projects/features/data_stack/__init__.py create mode 100644 src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/data_stack/application/__init__.py create mode 100644 src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc create mode 100644 src/mini_projects/features/data_stack/application/analysis.py create mode 100644 src/mini_projects/features/dsa/__init__.py create mode 100644 src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/dsa/application/__init__.py create mode 100644 src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc create mode 100644 src/mini_projects/features/dsa/application/algorithms.py create mode 100644 src/mini_projects/features/tictactoe/__init__.py create mode 100644 src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/application/__init__.py create mode 100644 src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/application/__pycache__/ai.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/application/ai.py create mode 100644 src/mini_projects/features/tictactoe/application/game.py create mode 100644 src/mini_projects/features/tictactoe/domain/__init__.py create mode 100644 src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/domain/board.py create mode 100644 src/mini_projects/features/tictactoe/presentation/__init__.py create mode 100644 src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/presentation/__pycache__/cli.cpython-313.pyc create mode 100644 src/mini_projects/features/tictactoe/presentation/cli.py create mode 100644 tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/conftest.py create mode 100644 tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/advanced_python/test_patterns.py create mode 100644 tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/async_concurrency/test_pipeline.py create mode 100644 tests/features/backend_api/__pycache__/test_app.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/backend_api/test_app.py create mode 100644 tests/features/big_data/__pycache__/test_chunked_processing.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/big_data/test_chunked_processing.py create mode 100644 tests/features/capstone/__pycache__/test_capstone_pipeline.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/capstone/__pycache__/test_world_bank_client.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/capstone/test_capstone_pipeline.py create mode 100644 tests/features/capstone/test_world_bank_client.py create mode 100644 tests/features/capstone/test_world_bank_refresh.py create mode 100644 tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/data_stack/test_analysis.py create mode 100644 tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/dsa/test_algorithms.py create mode 100644 tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/features/tictactoe/test_ai.py create mode 100644 tests/features/tictactoe/test_board.py create mode 100644 tests/test_cli.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e1d1e80 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run tests + env: + PYTHONPATH: src + run: pytest -q + diff --git a/Binary Search.ipynb b/Binary Search.ipynb index d9d296f..8f362e6 100644 --- a/Binary Search.ipynb +++ b/Binary Search.ipynb @@ -1,131 +1,254 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "70cc70ea", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Naive search time: 2.4266116619110107 seconds\n", - "Binary search time: 0.03394174575805664 seconds\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Algorithms Visualizer\n", + "\n", + "Goal: compare and trace core algorithms (search, traversal, sorting).\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Benchmark/trace runner\n", + "4. Quick verification checks" + ], + "id": "14c6f02c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Algorithms visualizer notebook: binary search, BFS, DFS, quicksort, mergesort.\"\"\"\n", + "\n", + "from collections import deque\n", + "from time import perf_counter\n", + "import random\n", + "\n", + "\n", + "def validate_sorted(values: list[int]) -> None:\n", + " if any(values[i] > values[i + 1] for i in range(len(values) - 1)):\n", + " raise ValueError(\"Input list must be sorted.\")\n", + "\n", + "\n", + "def binary_search_iterative(values: list[int], target: int) -> tuple[int, list[tuple[int, int, int]]]:\n", + " validate_sorted(values)\n", + " low, high = 0, len(values) - 1\n", + " trace: list[tuple[int, int, int]] = []\n", + " while low <= high:\n", + " mid = (low + high) // 2\n", + " trace.append((low, mid, high))\n", + " if values[mid] == target:\n", + " return mid, trace\n", + " if values[mid] < target:\n", + " low = mid + 1\n", + " else:\n", + " high = mid - 1\n", + " return -1, trace\n", + "\n", + "\n", + "def bfs(graph: dict[str, list[str]], start: str) -> list[str]:\n", + " visited, order = {start}, []\n", + " queue = deque([start])\n", + " while queue:\n", + " node = queue.popleft()\n", + " order.append(node)\n", + " for nxt in graph.get(node, []):\n", + " if nxt not in visited:\n", + " visited.add(nxt)\n", + " queue.append(nxt)\n", + " return order\n", + "\n", + "\n", + "def dfs(graph: dict[str, list[str]], start: str) -> list[str]:\n", + " visited, order = set(), []\n", + "\n", + " def walk(node: str) -> None:\n", + " visited.add(node)\n", + " order.append(node)\n", + " for nxt in graph.get(node, []):\n", + " if nxt not in visited:\n", + " walk(nxt)\n", + "\n", + " walk(start)\n", + " return order\n", + "\n", + "\n", + "def quicksort(values: list[int]) -> list[int]:\n", + " if len(values) <= 1:\n", + " return values\n", + " pivot = values[len(values) // 2]\n", + " left = [x for x in values if x < pivot]\n", + " middle = [x for x in values if x == pivot]\n", + " right = [x for x in values if x > pivot]\n", + " return quicksort(left) + middle + quicksort(right)\n", + "\n", + "\n", + "def mergesort(values: list[int]) -> list[int]:\n", + " if len(values) <= 1:\n", + " return values\n", + " mid = len(values) // 2\n", + " left = mergesort(values[:mid])\n", + " right = mergesort(values[mid:])\n", + " merged = []\n", + " i = j = 0\n", + " while i < len(left) and j < len(right):\n", + " if left[i] <= right[j]:\n", + " merged.append(left[i]); i += 1\n", + " else:\n", + " merged.append(right[j]); j += 1\n", + " return merged + left[i:] + right[j:]\n", + "\n", + "\n", + "def benchmark(search_fn, values: list[int], targets: list[int]) -> float:\n", + " start = perf_counter()\n", + " for t in targets:\n", + " search_fn(values, t)\n", + " return perf_counter() - start\n", + "\n", + "\n", + "def linear_search(values: list[int], target: int) -> int:\n", + " for i, value in enumerate(values):\n", + " if value == target:\n", + " return i\n", + " return -1\n", + "\n", + "\n", + "# Quick verification checks\n", + "nums = [1, 4, 8, 11, 15, 20]\n", + "idx, trace = binary_search_iterative(nums, 11)\n", + "assert idx == 3 and len(trace) > 0\n", + "assert bfs({\"A\": [\"B\", \"C\"], \"B\": [\"D\"], \"C\": [], \"D\": []}, \"A\") == [\"A\", \"B\", \"C\", \"D\"]\n", + "assert mergesort([5, 2, 7, 1]) == [1, 2, 5, 7]\n", + "\n", + "\n", + "# Deterministic benchmark and trace demo\n", + "rng = random.Random(11)\n", + "values = sorted(rng.sample(range(-5000, 5000), 1200))\n", + "targets = values[:200]\n", + "time_binary = benchmark(lambda arr, t: binary_search_iterative(arr, t)[0], values, targets)\n", + "time_naive = benchmark(linear_search, values, targets)\n", + "print(\"binary_search seconds:\", round(time_binary, 6))\n", + "print(\"linear_search seconds:\", round(time_naive, 6))\n", + "print(\"sample trace for target 20:\", binary_search_iterative(nums, 20)[1])\n", + "\n", + "# We will prove that binary search is faster than naive search!\n", + "\n", + "\n", + "# Essence of binary search:\n", + "# If you have a sorted list and you want to search this array for something,\n", + "# You could go through each item in the list and ask, is this equal to what we're looking for?\n", + "# But we can make this *faster* by leveraging the fact that our array is sorted!\n", + "# Binary search ~ O(log(n)), naive search ~ O(n)\n", + "\n", + "# In these two examples, l is a list in ascending order, and target is something that we're looking for\n", + "# Return -1 if not found\n", + "\n", + "\n", + "# naive search: scan entire list and ask if its equal to the target\n", + "# if yes, return the index\n", + "# if no, then return -1\n", + "\n", + "def naive_search(l, target):\n", + " # example l = [1, 3, 10, 12]\n", + " for i in range(len(l)):\n", + " if l[i] == target:\n", + " return i\n", + " return -1\n", + "\n", + "\n", + "# binary search uses divide and conquer!\n", + "# we will leverage the fact that our list is SORTED\n", + "def binary_search(l, target, low=None, high=None):\n", + " if low is None:\n", + " low = 0\n", + " if high is None:\n", + " high = len(l) - 1\n", + "\n", + " if high < low:\n", + " return -1\n", + "\n", + " # example l = [1, 3, 5, 10, 12] # should return 3\n", + " midpoint = (low + high) // 2 # 2\n", + "\n", + " # we'll check if l[midpoint] == target, and if not, we can find out if\n", + " # target will be to the left or right of midpoint\n", + " # we know everything to the left of midpoint is smaller than the midpoint\n", + " # and everything to the right is larger\n", + " if l[midpoint] == target:\n", + " return midpoint\n", + " elif target < l[midpoint]:\n", + " return binary_search(l, target, low, midpoint-1)\n", + " else:\n", + " # target > l[midpoint]\n", + " return binary_search(l, target, midpoint+1, high)\n", + "\n", + "if __name__=='__main__':\n", + " # l = [1, 3, 5, 10, 12]\n", + " # target = 7\n", + " # print(naive_search(l, target))\n", + " # print(binary_search(l, target))\n", + "\n", + " length = 10000\n", + " # build a sorted list of length 10000\n", + " sorted_list = set()\n", + " while len(sorted_list) < length:\n", + " sorted_list.add(random.randint(-3*length, 3*length))\n", + " sorted_list = sorted(list(sorted_list))\n", + "\n", + " start = time.time()\n", + " for target in sorted_list:\n", + " naive_search(sorted_list, target)\n", + " end = time.time()\n", + " print(\"Naive search time: \", (end - start), \"seconds\")\n", + "\n", + " start = time.time()\n", + " for target in sorted_list:\n", + " binary_search(sorted_list, target)\n", + " end = time.time()\n", + " print(\"Binary search time: \", (end - start), \"seconds\")" + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Naive search time: 2.4266116619110107 seconds\n", + "Binary search time: 0.03394174575805664 seconds\n" + ] + } + ], + "id": "70cc70ea" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "1750bde4" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "import random\n", - "import time\n", - "\n", - "# Implementation of binary search algorithm!!\n", - "\n", - "# We will prove that binary search is faster than naive search!\n", - "\n", - "\n", - "# Essence of binary search:\n", - "# If you have a sorted list and you want to search this array for something,\n", - "# You could go through each item in the list and ask, is this equal to what we're looking for?\n", - "# But we can make this *faster* by leveraging the fact that our array is sorted!\n", - "# Binary search ~ O(log(n)), naive search ~ O(n)\n", - "\n", - "# In these two examples, l is a list in ascending order, and target is something that we're looking for\n", - "# Return -1 if not found\n", - "\n", - "\n", - "# naive search: scan entire list and ask if its equal to the target\n", - "# if yes, return the index\n", - "# if no, then return -1\n", - "\n", - "def naive_search(l, target):\n", - " # example l = [1, 3, 10, 12]\n", - " for i in range(len(l)):\n", - " if l[i] == target:\n", - " return i\n", - " return -1\n", - "\n", - "\n", - "# binary search uses divide and conquer!\n", - "# we will leverage the fact that our list is SORTED\n", - "def binary_search(l, target, low=None, high=None):\n", - " if low is None:\n", - " low = 0\n", - " if high is None:\n", - " high = len(l) - 1\n", - "\n", - " if high < low:\n", - " return -1\n", - "\n", - " # example l = [1, 3, 5, 10, 12] # should return 3\n", - " midpoint = (low + high) // 2 # 2\n", - "\n", - " # we'll check if l[midpoint] == target, and if not, we can find out if\n", - " # target will be to the left or right of midpoint\n", - " # we know everything to the left of midpoint is smaller than the midpoint\n", - " # and everything to the right is larger\n", - " if l[midpoint] == target:\n", - " return midpoint\n", - " elif target < l[midpoint]:\n", - " return binary_search(l, target, low, midpoint-1)\n", - " else:\n", - " # target > l[midpoint]\n", - " return binary_search(l, target, midpoint+1, high)\n", - "\n", - "if __name__=='__main__':\n", - " # l = [1, 3, 5, 10, 12]\n", - " # target = 7\n", - " # print(naive_search(l, target))\n", - " # print(binary_search(l, target))\n", - "\n", - " length = 10000\n", - " # build a sorted list of length 10000\n", - " sorted_list = set()\n", - " while len(sorted_list) < length:\n", - " sorted_list.add(random.randint(-3*length, 3*length))\n", - " sorted_list = sorted(list(sorted_list))\n", - "\n", - " start = time.time()\n", - " for target in sorted_list:\n", - " naive_search(sorted_list, target)\n", - " end = time.time()\n", - " print(\"Naive search time: \", (end - start), \"seconds\")\n", - "\n", - " start = time.time()\n", - " for target in sorted_list:\n", - " binary_search(sorted_list, target)\n", - " end = time.time()\n", - " print(\"Binary search time: \", (end - start), \"seconds\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1750bde4", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/Find the month.ipynb b/Find the month.ipynb index dbcf40d..fb1d6a8 100644 --- a/Find the month.ipynb +++ b/Find the month.ipynb @@ -1,66 +1,111 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 4, - "id": "dac8d791", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "November\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Find the Month (Foundations)\n", + "\n", + "Goal: practice dictionary lookups, normalization, and safe fallback behavior.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Quick verification checks\n", + "4. Demo outputs" + ], + "id": "47252409" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Foundation notebook: safe month lookups with normalization.\"\"\"\n", + "\n", + "from difflib import get_close_matches\n", + "\n", + "MONTH_CONVERSIONS = {\n", + " \"JAN\": \"January\",\n", + " \"FEB\": \"February\",\n", + " \"MAR\": \"March\",\n", + " \"APR\": \"April\",\n", + " \"MAY\": \"May\",\n", + " \"JUN\": \"June\",\n", + " \"JUL\": \"July\",\n", + " \"AUG\": \"August\",\n", + " \"SEP\": \"September\",\n", + " \"OCT\": \"October\",\n", + " \"NOV\": \"November\",\n", + " \"DEC\": \"December\",\n", + "}\n", + "\n", + "\n", + "def normalize_month_key(raw: str) -> str:\n", + " return raw.strip().upper()[:3]\n", + "\n", + "\n", + "def expand_month(raw: str) -> str:\n", + " key = normalize_month_key(raw)\n", + " return MONTH_CONVERSIONS.get(key, \"Not a valid key\")\n", + "\n", + "\n", + "def suggest_month_key(raw: str) -> str | None:\n", + " key = normalize_month_key(raw)\n", + " matches = get_close_matches(key, MONTH_CONVERSIONS.keys(), n=1, cutoff=0.6)\n", + " return matches[0] if matches else None\n", + "\n", + "\n", + "# Quick verification checks\n", + "assert expand_month(\"Nov\") == \"November\"\n", + "assert expand_month(\" february\") == \"February\"\n", + "assert expand_month(\"Luv\") == \"Not a valid key\"\n", + "\n", + "\n", + "print(\"Valid lookup:\", expand_month(\"Nov\"))\n", + "print(\"Invalid lookup:\", expand_month(\"Luv\"))\n", + "maybe = suggest_month_key(\"Luv\")\n", + "if maybe:\n", + " print(\"Did you mean:\", MONTH_CONVERSIONS[maybe])" + ], + "execution_count": 4, + "outputs": [ + { + "output_type": "stream", + "text": [ + "November\n" + ] + } + ], + "id": "dac8d791" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "ef4aad7f" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "monthConversions = {\n", - " \"Jan\" : \"January\" ,\n", - " \"Feb\" : \"February\" ,\n", - " \"Mar\" : \"March\" ,\n", - " \"Apr\" : \"April\" ,\n", - " \"May\" : \"May\" ,\n", - " \"Jun\" : \"June\" ,\n", - " \"Jul\" : \"July\" ,\n", - " \"Aug\" : \"August\" ,\n", - " \"Sep\" : \"September\" ,\n", - " \"Oct\" : \"October\" ,\n", - " \"Nov\" : \"November\" ,\n", - " \"Dec\" : \"December\" ,\n", - "}\n", - "\n", - "print(monthConversions.get(\"Luv\" , \"Not a valid key\"))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef4aad7f", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/Guess the number.ipynb b/Guess the number.ipynb index 46b9f85..7a5f484 100644 --- a/Guess the number.ipynb +++ b/Guess the number.ipynb @@ -1,80 +1,144 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 3, - "id": "86407d0c", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Guess a number between 1 and 10: 10\n", - "Sorry, guess again. Too high.\n", - "Guess a number between 1 and 10: 5\n", - "Sorry, guess again. Too low.\n", - "Guess a number between 1 and 10: 7\n", - "Sorry, guess again. Too high.\n", - "Guess a number between 1 and 10: 6\n", - "Yay, congrats. You have guessed the number 6 correctly!!\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Guess the Number\n", + "\n", + "Goal: robust guessing game with validation and reverse mode.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Gameplay runners\n", + "4. Quick verification checks" + ], + "id": "aa37b3a0" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Guess-the-number notebook with validation, replay helpers, and reverse mode.\"\"\"\n", + "\n", + "import random\n", + "\n", + "\n", + "def parse_guess(text: str) -> int:\n", + " value = int(text.strip())\n", + " return value\n", + "\n", + "\n", + "def get_feedback(guess: int, target: int) -> str:\n", + " if guess < target:\n", + " return \"low\"\n", + " if guess > target:\n", + " return \"high\"\n", + " return \"correct\"\n", + "\n", + "\n", + "def play_guess(max_value: int, rng_seed: int = 0, input_fn=input, output_fn=print) -> int:\n", + " if max_value < 2:\n", + " raise ValueError(\"max_value must be at least 2\")\n", + " rng = random.Random(rng_seed)\n", + " target = rng.randint(1, max_value)\n", + " attempts = 0\n", + "\n", + " while True:\n", + " raw = input_fn(f\"Guess a number between 1 and {max_value}: \")\n", + " try:\n", + " guess = parse_guess(raw)\n", + " except ValueError:\n", + " output_fn(\"Please enter a whole number.\")\n", + " continue\n", + " if not (1 <= guess <= max_value):\n", + " output_fn(\"Guess out of range.\")\n", + " continue\n", + " attempts += 1\n", + " feedback = get_feedback(guess, target)\n", + " if feedback == \"correct\":\n", + " output_fn(f\"Correct! Target was {target}. Attempts: {attempts}\")\n", + " return attempts\n", + " output_fn(\"Too low.\" if feedback == \"low\" else \"Too high.\")\n", + "\n", + "\n", + "def computer_guesses(user_target: int, max_value: int) -> int:\n", + " low, high = 1, max_value\n", + " attempts = 0\n", + " while low <= high:\n", + " attempts += 1\n", + " mid = (low + high) // 2\n", + " if mid == user_target:\n", + " return attempts\n", + " if mid < user_target:\n", + " low = mid + 1\n", + " else:\n", + " high = mid - 1\n", + " raise RuntimeError(\"Target should always be found in range.\")\n", + "\n", + "\n", + "# Quick verification checks\n", + "assert get_feedback(3, 5) == \"low\"\n", + "assert get_feedback(8, 5) == \"high\"\n", + "assert computer_guesses(73, 100) <= 7\n", + "\n", + "print(\"Reverse mode demo attempts for target 42:\", computer_guesses(42, 100))" + ], + "execution_count": 3, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Guess a number between 1 and 10: 10\n", + "Sorry, guess again. Too high.\n", + "Guess a number between 1 and 10: 5\n", + "Sorry, guess again. Too low.\n", + "Guess a number between 1 and 10: 7\n", + "Sorry, guess again. Too high.\n", + "Guess a number between 1 and 10: 6\n", + "Yay, congrats. You have guessed the number 6 correctly!!\n" + ] + } + ], + "id": "86407d0c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "f33aca11" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "95dd1c3c" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "import random\n", - "\n", - "def guess(x):\n", - " random_number = random.randint(1, x)\n", - " guess = 0\n", - " while guess != random_number : \n", - " guess = int(input (f'Guess a number between 1 and {x}: '))\n", - " if guess < random_number:\n", - " print('Sorry, guess again. Too low.')\n", - " elif guess > random_number:\n", - " print('Sorry, guess again. Too high.')\n", - " print(f'Yay, congrats. You have guessed the number {random_number} correctly!!')\n", - "\n", - "guess(10)\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f33aca11", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95dd1c3c", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/Hangman.ipynb b/Hangman.ipynb index 58961ca..b4e6226 100644 --- a/Hangman.ipynb +++ b/Hangman.ipynb @@ -1,237 +1,344 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 8, - "id": "0daac62b", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "You have 7 lives left and you have used these letters: \n", - "Current word: - - - - - - - -\n", - "Guess a letter: A\n", - "\n", - "Your letter, A is not in the word.\n", - "You have 6 lives left and you have used these letters: A\n", - "Current word: - - - - - - - -\n", - "Guess a letter: B\n", - "\n", - "Your letter, B is not in the word.\n", - "You have 5 lives left and you have used these letters: A B\n", - "Current word: - - - - - - - -\n", - "Guess a letter: E\n", - "\n", - "You have 5 lives left and you have used these letters: A E B\n", - "Current word: - - - - - - - E\n", - "Guess a letter: P\n", - "\n", - "You have 5 lives left and you have used these letters: A E P B\n", - "Current word: - - - P - - - E\n", - "Guess a letter: M\n", - "\n", - "Your letter, M is not in the word.\n", - "You have 4 lives left and you have used these letters: E A M B P\n", - "Current word: - - - P - - - E\n", - "Guess a letter: N\n", - "\n", - "Your letter, N is not in the word.\n", - "You have 3 lives left and you have used these letters: E A M B N P\n", - "Current word: - - - P - - - E\n", - "Guess a letter: C\n", - "\n", - "Your letter, C is not in the word.\n", - "You have 2 lives left and you have used these letters: E A M B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: D\n", - "\n", - "Your letter, D is not in the word.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: E\n", - "\n", - "You have already used that letter. Guess another letter.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: A\n", - "\n", - "You have already used that letter. Guess another letter.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: C\n", - "\n", - "You have already used that letter. Guess another letter.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: B\n", - "\n", - "You have already used that letter. Guess another letter.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: N\n", - "\n", - "You have already used that letter. Guess another letter.\n", - "You have 1 lives left and you have used these letters: E A M D B N P C\n", - "Current word: - - - P - - - E\n", - "Guess a letter: g\n", - "\n", - "Your letter, G is not in the word.\n", - "You died, sorry. The word was SURPRISE\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hangman\n", + "\n", + "Goal: category-based gameplay with persistence and hints.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Gameplay runners\n", + "4. Quick verification checks" + ], + "id": "e1cc1628" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Hangman notebook with categories, hints, multiplayer, and score persistence.\"\"\"\n", + "\n", + "from collections import Counter\n", + "import json\n", + "import random\n", + "import string\n", + "from pathlib import Path\n", + "\n", + "from words import words as WORD_BANK\n", + "\n", + "SCORES_PATH = Path(\"hangman_scores.json\")\n", + "CATEGORIES = {\n", + " \"short\": [w for w in WORD_BANK if 4 <= len(w) <= 6],\n", + " \"medium\": [w for w in WORD_BANK if 7 <= len(w) <= 9],\n", + " \"long\": [w for w in WORD_BANK if len(w) >= 10],\n", + "}\n", + "\n", + "\n", + "def get_valid_word(category: str, rng: random.Random) -> str:\n", + " choices = CATEGORIES.get(category, WORD_BANK)\n", + " pool = [w for w in choices if \"-\" not in w and \" \" not in w]\n", + " return rng.choice(pool).upper()\n", + "\n", + "\n", + "def render_masked_word(word: str, used_letters: set[str]) -> str:\n", + " return \" \".join([letter if letter in used_letters else \"-\" for letter in word])\n", + "\n", + "\n", + "def recommend_letter(word: str, used_letters: set[str]) -> str | None:\n", + " unseen = [c for c in word if c not in used_letters]\n", + " if not unseen:\n", + " return None\n", + " counts = Counter(unseen)\n", + " return counts.most_common(1)[0][0]\n", + "\n", + "\n", + "def update_scoreboard(player: str, score: int) -> dict[str, int]:\n", + " data = {}\n", + " if SCORES_PATH.exists():\n", + " data = json.loads(SCORES_PATH.read_text())\n", + " data[player] = max(score, data.get(player, 0))\n", + " SCORES_PATH.write_text(json.dumps(data, indent=2))\n", + " return data\n", + "\n", + "\n", + "def play_round(player: str, category: str = \"medium\", lives: int = 7, rng_seed: int = 0) -> int:\n", + " rng = random.Random(rng_seed)\n", + " word = get_valid_word(category, rng)\n", + " word_letters = set(word)\n", + " used_letters: set[str] = set()\n", + " alphabet = set(string.ascii_uppercase)\n", + "\n", + " while word_letters and lives > 0:\n", + " print(f\"Lives: {lives}; Used: {' '.join(sorted(used_letters))}\")\n", + " print(render_masked_word(word, used_letters))\n", + " hint = recommend_letter(word, used_letters)\n", + " if hint:\n", + " print(f\"Hint letter: {hint}\")\n", + "\n", + " user_letter = input(\"Guess letter: \").upper().strip()\n", + " if user_letter in alphabet - used_letters:\n", + " used_letters.add(user_letter)\n", + " if user_letter in word_letters:\n", + " word_letters.remove(user_letter)\n", + " else:\n", + " lives -= 1\n", + " elif user_letter in used_letters:\n", + " print(\"Already used.\")\n", + " else:\n", + " print(\"Invalid character.\")\n", + "\n", + " score = lives * 10 + len(set(word) & used_letters)\n", + " if lives > 0:\n", + " print(f\"{player} won. Word: {word}\")\n", + " else:\n", + " print(f\"{player} lost. Word was: {word}\")\n", + " board = update_scoreboard(player, score)\n", + " print(\"Top scores:\", dict(sorted(board.items(), key=lambda x: x[1], reverse=True)[:5]))\n", + " return score\n", + "\n", + "\n", + "def play_multiplayer(players: list[str], category: str = \"medium\") -> dict[str, int]:\n", + " results = {}\n", + " for idx, name in enumerate(players):\n", + " print(f\"\\n--- Turn: {name} ---\")\n", + " results[name] = play_round(name, category=category, rng_seed=idx + 1)\n", + " return results\n", + "\n", + "\n", + "# Quick verification checks\n", + "assert render_masked_word(\"CODE\", {\"C\", \"E\"}) == \"C - - E\"\n", + "assert recommend_letter(\"BANANA\", {\"B\"}) in {\"A\", \"N\"}\n", + "assert get_valid_word(\"short\", random.Random(1)).isalpha()\n", + "\n", + "print(\"Hangman module ready. Call play_round('Player1') or play_multiplayer([...]).\")\n", + "\n", + "\n", + "def get_valid_word(words):\n", + " word = random.choice(words) # randomly chooses something from the list\n", + " while '-' in word or ' ' in word:\n", + " word = random.choice(words)\n", + "\n", + " return word.upper()\n", + "\n", + "\n", + "def hangman():\n", + " word = get_valid_word(words)\n", + " word_letters = set(word) # letters in the word\n", + " alphabet = set(string.ascii_uppercase)\n", + " used_letters = set() # what the user has guessed\n", + "\n", + " lives = 7\n", + "\n", + " # getting user input\n", + " while len(word_letters) > 0 and lives > 0:\n", + " # letters used\n", + " # ' '.join(['a', 'b', 'cd']) --> 'a b cd'\n", + " print('You have', lives, 'lives left and you have used these letters: ', ' '.join(used_letters))\n", + "\n", + " # what current word is (ie W - R D)\n", + " word_list = [letter if letter in used_letters else '-' for letter in word]\n", + " print('Current word: ', ' '.join(word_list))\n", + "\n", + " user_letter = input('Guess a letter: ').upper()\n", + " if user_letter in alphabet - used_letters:\n", + " used_letters.add(user_letter)\n", + " if user_letter in word_letters:\n", + " word_letters.remove(user_letter)\n", + " print('')\n", + "\n", + " else:\n", + " lives = lives - 1 # takes away a life if wrong\n", + " print('\\nYour letter,', user_letter, 'is not in the word.')\n", + "\n", + " elif user_letter in used_letters:\n", + " print('\\nYou have already used that letter. Guess another letter.')\n", + "\n", + " else:\n", + " print('\\nThat is not a valid letter.')\n", + "\n", + " # gets here when len(word_letters) == 0 OR when lives == 0\n", + " if lives == 0:\n", + " print('You died, sorry. The word was', word)\n", + " else:\n", + " print('YAY! You guessed the word', word, '!!')\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " hangman()" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "text": [ + "You have 7 lives left and you have used these letters: \n", + "Current word: - - - - - - - -\n", + "Guess a letter: A\n", + "\n", + "Your letter, A is not in the word.\n", + "You have 6 lives left and you have used these letters: A\n", + "Current word: - - - - - - - -\n", + "Guess a letter: B\n", + "\n", + "Your letter, B is not in the word.\n", + "You have 5 lives left and you have used these letters: A B\n", + "Current word: - - - - - - - -\n", + "Guess a letter: E\n", + "\n", + "You have 5 lives left and you have used these letters: A E B\n", + "Current word: - - - - - - - E\n", + "Guess a letter: P\n", + "\n", + "You have 5 lives left and you have used these letters: A E P B\n", + "Current word: - - - P - - - E\n", + "Guess a letter: M\n", + "\n", + "Your letter, M is not in the word.\n", + "You have 4 lives left and you have used these letters: E A M B P\n", + "Current word: - - - P - - - E\n", + "Guess a letter: N\n", + "\n", + "Your letter, N is not in the word.\n", + "You have 3 lives left and you have used these letters: E A M B N P\n", + "Current word: - - - P - - - E\n", + "Guess a letter: C\n", + "\n", + "Your letter, C is not in the word.\n", + "You have 2 lives left and you have used these letters: E A M B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: D\n", + "\n", + "Your letter, D is not in the word.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: E\n", + "\n", + "You have already used that letter. Guess another letter.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: A\n", + "\n", + "You have already used that letter. Guess another letter.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: C\n", + "\n", + "You have already used that letter. Guess another letter.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: B\n", + "\n", + "You have already used that letter. Guess another letter.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: N\n", + "\n", + "You have already used that letter. Guess another letter.\n", + "You have 1 lives left and you have used these letters: E A M D B N P C\n", + "Current word: - - - P - - - E\n", + "Guess a letter: g\n", + "\n", + "Your letter, G is not in the word.\n", + "You died, sorry. The word was SURPRISE\n" + ] + } + ], + "id": "0daac62b" + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [], + "id": "c29b51a8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "a506eb51" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "88c34b1d" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "53a3054b" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "fb143309" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "e58a5a3c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "239143f9" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "03007b2c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "2cbec189" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "import random\n", - "from words import words\n", - "import string\n", - "\n", - "\n", - "def get_valid_word(words):\n", - " word = random.choice(words) # randomly chooses something from the list\n", - " while '-' in word or ' ' in word:\n", - " word = random.choice(words)\n", - "\n", - " return word.upper()\n", - "\n", - "\n", - "def hangman():\n", - " word = get_valid_word(words)\n", - " word_letters = set(word) # letters in the word\n", - " alphabet = set(string.ascii_uppercase)\n", - " used_letters = set() # what the user has guessed\n", - "\n", - " lives = 7\n", - "\n", - " # getting user input\n", - " while len(word_letters) > 0 and lives > 0:\n", - " # letters used\n", - " # ' '.join(['a', 'b', 'cd']) --> 'a b cd'\n", - " print('You have', lives, 'lives left and you have used these letters: ', ' '.join(used_letters))\n", - "\n", - " # what current word is (ie W - R D)\n", - " word_list = [letter if letter in used_letters else '-' for letter in word]\n", - " print('Current word: ', ' '.join(word_list))\n", - "\n", - " user_letter = input('Guess a letter: ').upper()\n", - " if user_letter in alphabet - used_letters:\n", - " used_letters.add(user_letter)\n", - " if user_letter in word_letters:\n", - " word_letters.remove(user_letter)\n", - " print('')\n", - "\n", - " else:\n", - " lives = lives - 1 # takes away a life if wrong\n", - " print('\\nYour letter,', user_letter, 'is not in the word.')\n", - "\n", - " elif user_letter in used_letters:\n", - " print('\\nYou have already used that letter. Guess another letter.')\n", - "\n", - " else:\n", - " print('\\nThat is not a valid letter.')\n", - "\n", - " # gets here when len(word_letters) == 0 OR when lives == 0\n", - " if lives == 0:\n", - " print('You died, sorry. The word was', word)\n", - " else:\n", - " print('YAY! You guessed the word', word, '!!')\n", - "\n", - "\n", - "if __name__ == '__main__':\n", - " hangman()" - ] - }, - { - "cell_type": "raw", - "id": "c29b51a8", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a506eb51", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88c34b1d", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53a3054b", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb143309", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e58a5a3c", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "239143f9", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03007b2c", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2cbec189", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/Minesweeper.ipynb b/Minesweeper.ipynb index 6812494..dffa01e 100644 --- a/Minesweeper.ipynb +++ b/Minesweeper.ipynb @@ -1,348 +1,550 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 5, - "id": "1d0237c9", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 | | | | | | | | | | |\n", - "1 | | | | | | | | | | |\n", - "2 | | | | | | | | | | |\n", - "3 | | | | | | | | | | |\n", - "4 | | | | | | | | | | |\n", - "5 | | | | | | | | | | |\n", - "6 | | | | | | | | | | |\n", - "7 | | | | | | | | | | |\n", - "8 | | | | | | | | | | |\n", - "9 | | | | | | | | | | |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 0,0\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 | | | | | | | | | |\n", - "1 | | | | | | | | | | |\n", - "2 | | | | | | | | | | |\n", - "3 | | | | | | | | | | |\n", - "4 | | | | | | | | | | |\n", - "5 | | | | | | | | | | |\n", - "6 | | | | | | | | | | |\n", - "7 | | | | | | | | | | |\n", - "8 | | | | | | | | | | |\n", - "9 | | | | | | | | | | |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 0,1\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 |1 | | | | | | | | |\n", - "1 | | | | | | | | | | |\n", - "2 | | | | | | | | | | |\n", - "3 | | | | | | | | | | |\n", - "4 | | | | | | | | | | |\n", - "5 | | | | | | | | | | |\n", - "6 | | | | | | | | | | |\n", - "7 | | | | | | | | | | |\n", - "8 | | | | | | | | | | |\n", - "9 | | | | | | | | | | |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 9,9\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 |1 | | | | | | | | |\n", - "1 | | | | | | | |1 |1 |1 |\n", - "2 | | | | | | | |1 |0 |0 |\n", - "3 | | | | | | | |1 |0 |0 |\n", - "4 | | | | | | |2 |1 |0 |0 |\n", - "5 | | | | | | |2 |0 |0 |0 |\n", - "6 | | | | | | |2 |0 |0 |0 |\n", - "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", - "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 3,3\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 |1 |1 |0 |0 |0 |0 |1 | | |\n", - "1 | | |1 |0 |0 |0 |0 |1 |1 |1 |\n", - "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", - "3 |0 |0 |0 |0 |0 |1 | |1 |0 |0 |\n", - "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", - "5 | | |2 |1 |2 | |2 |0 |0 |0 |\n", - "6 | | | | | | |2 |0 |0 |0 |\n", - "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", - "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 0,9\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 |1 |1 |0 |0 |0 |0 |1 | |1 |\n", - "1 | | |1 |0 |0 |0 |0 |1 |1 |1 |\n", - "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", - "3 |0 |0 |0 |0 |0 |1 | |1 |0 |0 |\n", - "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", - "5 | | |2 |1 |2 | |2 |0 |0 |0 |\n", - "6 | | | | | | |2 |0 |0 |0 |\n", - "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", - "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "----------------------------------\n", - "Where would you like to dig? Input as row,col: 9,0\n", - "SORRY GAME OVER :(\n", - " 0 1 2 3 4 5 6 7 8 9 \n", - "----------------------------------\n", - "0 |1 |1 |1 |0 |0 |0 |0 |1 |* |1 |\n", - "1 |1 |* |1 |0 |0 |0 |0 |1 |1 |1 |\n", - "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", - "3 |0 |0 |0 |0 |0 |1 |* |1 |0 |0 |\n", - "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", - "5 |2 |* |2 |1 |2 |* |2 |0 |0 |0 |\n", - "6 |* |3 |* |1 |2 |* |2 |0 |0 |0 |\n", - "7 |1 |2 |1 |1 |1 |1 |1 |0 |0 |0 |\n", - "8 |2 |2 |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "9 |* |* |1 |0 |0 |0 |0 |0 |0 |0 |\n", - "----------------------------------\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Minesweeper\n", + "\n", + "Goal: complete playable game with flags, timer, leaderboard, and hints.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions and board model\n", + "3. Gameplay runner\n", + "4. Quick verification checks" + ], + "id": "8b84991c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Minesweeper notebook with flags, timer, SQLite leaderboard, and solver assist.\"\"\"\n", + "\n", + "from dataclasses import dataclass, field\n", + "from pathlib import Path\n", + "import random\n", + "import sqlite3\n", + "import time\n", + "\n", + "DB_PATH = Path(\"minesweeper_scores.db\")\n", + "\n", + "\n", + "def init_db() -> None:\n", + " conn = sqlite3.connect(DB_PATH)\n", + " conn.execute(\n", + " \"\"\"\n", + " CREATE TABLE IF NOT EXISTS scores (\n", + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n", + " player TEXT NOT NULL,\n", + " dim_size INTEGER NOT NULL,\n", + " bombs INTEGER NOT NULL,\n", + " elapsed_seconds REAL NOT NULL,\n", + " won INTEGER NOT NULL,\n", + " created_at TEXT DEFAULT CURRENT_TIMESTAMP\n", + " )\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " conn.close()\n", + "\n", + "\n", + "def save_score(player: str, dim_size: int, bombs: int, elapsed_seconds: float, won: bool) -> None:\n", + " conn = sqlite3.connect(DB_PATH)\n", + " conn.execute(\n", + " \"INSERT INTO scores (player, dim_size, bombs, elapsed_seconds, won) VALUES (?, ?, ?, ?, ?)\",\n", + " (player, dim_size, bombs, elapsed_seconds, int(won)),\n", + " )\n", + " conn.commit()\n", + " conn.close()\n", + "\n", + "\n", + "@dataclass\n", + "class Board:\n", + " dim_size: int\n", + " num_bombs: int\n", + " rng_seed: int = 0\n", + " board: list[list[object]] = field(init=False)\n", + " dug: set[tuple[int, int]] = field(default_factory=set)\n", + " flagged: set[tuple[int, int]] = field(default_factory=set)\n", + "\n", + " def __post_init__(self) -> None:\n", + " self.rng = random.Random(self.rng_seed)\n", + " self.board = self.make_new_board()\n", + " self.assign_values_to_board()\n", + "\n", + " def make_new_board(self) -> list[list[object]]:\n", + " board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)]\n", + " bombs_planted = 0\n", + " while bombs_planted < self.num_bombs:\n", + " loc = self.rng.randint(0, self.dim_size**2 - 1)\n", + " row, col = divmod(loc, self.dim_size)\n", + " if board[row][col] == \"*\":\n", + " continue\n", + " board[row][col] = \"*\"\n", + " bombs_planted += 1\n", + " return board\n", + "\n", + " def assign_values_to_board(self) -> None:\n", + " for r in range(self.dim_size):\n", + " for c in range(self.dim_size):\n", + " if self.board[r][c] == \"*\":\n", + " continue\n", + " self.board[r][c] = self.get_num_neighboring_bombs(r, c)\n", + "\n", + " def get_num_neighboring_bombs(self, row: int, col: int) -> int:\n", + " num = 0\n", + " for r in range(max(0, row - 1), min(self.dim_size - 1, row + 1) + 1):\n", + " for c in range(max(0, col - 1), min(self.dim_size - 1, col + 1) + 1):\n", + " if r == row and c == col:\n", + " continue\n", + " if self.board[r][c] == \"*\":\n", + " num += 1\n", + " return num\n", + "\n", + " def toggle_flag(self, row: int, col: int) -> None:\n", + " if (row, col) in self.dug:\n", + " return\n", + " if (row, col) in self.flagged:\n", + " self.flagged.remove((row, col))\n", + " else:\n", + " self.flagged.add((row, col))\n", + "\n", + " def dig(self, row: int, col: int) -> bool:\n", + " if (row, col) in self.flagged:\n", + " return True\n", + " self.dug.add((row, col))\n", + " if self.board[row][col] == \"*\":\n", + " return False\n", + " if self.board[row][col] > 0:\n", + " return True\n", + " # iterative flood fill\n", + " stack = [(row, col)]\n", + " while stack:\n", + " r0, c0 = stack.pop()\n", + " for r in range(max(0, r0 - 1), min(self.dim_size - 1, r0 + 1) + 1):\n", + " for c in range(max(0, c0 - 1), min(self.dim_size - 1, c0 + 1) + 1):\n", + " if (r, c) in self.dug or (r, c) in self.flagged:\n", + " continue\n", + " self.dug.add((r, c))\n", + " if self.board[r][c] == 0:\n", + " stack.append((r, c))\n", + " return True\n", + "\n", + " def solver_hint(self) -> tuple[int, int] | None:\n", + " safe = []\n", + " for r in range(self.dim_size):\n", + " for c in range(self.dim_size):\n", + " if (r, c) in self.dug or (r, c) in self.flagged:\n", + " continue\n", + " if self.board[r][c] != \"*\":\n", + " safe.append((r, c))\n", + " return safe[0] if safe else None\n", + "\n", + " def __str__(self) -> str:\n", + " visible = []\n", + " for r in range(self.dim_size):\n", + " row = []\n", + " for c in range(self.dim_size):\n", + " if (r, c) in self.flagged:\n", + " row.append(\"F\")\n", + " elif (r, c) not in self.dug:\n", + " row.append(\" \")\n", + " else:\n", + " row.append(str(self.board[r][c]))\n", + " visible.append(\" | \".join(row))\n", + " return \"\\n\".join(visible)\n", + "\n", + "\n", + "def parse_move(text: str, dim_size: int) -> tuple[str, int, int]:\n", + " parts = text.strip().split()\n", + " if len(parts) != 3:\n", + " raise ValueError(\"Use: \")\n", + " action, r, c = parts[0].lower(), int(parts[1]), int(parts[2])\n", + " if action not in {\"d\", \"f\", \"h\"}:\n", + " raise ValueError(\"Action must be d (dig), f (flag), or h (hint).\")\n", + " if action in {\"d\", \"f\"} and not (0 <= r < dim_size and 0 <= c < dim_size):\n", + " raise ValueError(\"Row/col out of range\")\n", + " return action, r, c\n", + "\n", + "\n", + "def play_minesweeper(dim_size: int = 8, bombs: int = 10, player: str = \"Player\") -> None:\n", + " init_db()\n", + " board = Board(dim_size, bombs, rng_seed=4)\n", + " start = time.time()\n", + "\n", + " while len(board.dug) < board.dim_size**2 - board.num_bombs:\n", + " print(board)\n", + " raw = input(\"Move ( row col): \")\n", + " try:\n", + " action, row, col = parse_move(raw, dim_size)\n", + " except Exception as exc:\n", + " print(exc)\n", + " continue\n", + "\n", + " if action == \"h\":\n", + " hint = board.solver_hint()\n", + " print(\"Hint safe cell:\", hint)\n", + " continue\n", + " if action == \"f\":\n", + " board.toggle_flag(row, col)\n", + " continue\n", + " if not board.dig(row, col):\n", + " elapsed = time.time() - start\n", + " print(\"Game over! You hit a bomb.\")\n", + " save_score(player, dim_size, bombs, elapsed, won=False)\n", + " return\n", + "\n", + " elapsed = time.time() - start\n", + " print(\"You won in\", round(elapsed, 2), \"seconds\")\n", + " save_score(player, dim_size, bombs, elapsed, won=True)\n", + "\n", + "\n", + "# Quick verification checks\n", + "init_db()\n", + "b = Board(5, 5, rng_seed=1)\n", + "assert isinstance(b.solver_hint(), tuple)\n", + "assert parse_move(\"d 1 2\", 5) == (\"d\", 1, 2)\n", + "assert len([1 for row in b.board for v in row if v == \"*\"]) == 5\n", + "\n", + "print(\"Minesweeper module ready. Call play_minesweeper().\")\n", + "\n", + "# lets create a board object to represent the minesweeper game\n", + "# this is so that we can just say \"create a new board object\", or\n", + "# \"dig here\", or \"render this game for this object\"\n", + "class Board:\n", + " def __init__(self, dim_size, num_bombs):\n", + " # let's keep track of these parameters. they'll be helpful later\n", + " self.dim_size = dim_size\n", + " self.num_bombs = num_bombs\n", + "\n", + " # let's create the board\n", + " # helper function!\n", + " self.board = self.make_new_board() # plant the bombs\n", + " self.assign_values_to_board()\n", + "\n", + " # initialize a set to keep track of which locations we've uncovered\n", + " # we'll save (row,col) tuples into this set \n", + " self.dug = set() # if we dig at 0, 0, then self.dug = {(0,0)}\n", + "\n", + " def make_new_board(self):\n", + " # construct a new board based on the dim size and num bombs\n", + " # we should construct the list of lists here (or whatever representation you prefer,\n", + " # but since we have a 2-D board, list of lists is most natural)\n", + "\n", + " # generate a new board\n", + " board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)]\n", + " # this creates an array like this:\n", + " # [[None, None, ..., None],\n", + " # [None, None, ..., None],\n", + " # [... ],\n", + " # [None, None, ..., None]]\n", + " # we can see how this represents a board!\n", + "\n", + " # plant the bombs\n", + " bombs_planted = 0\n", + " while bombs_planted < self.num_bombs:\n", + " loc = random.randint(0, self.dim_size**2 - 1) # return a random integer N such that a <= N <= b\n", + " row = loc // self.dim_size # we want the number of times dim_size goes into loc to tell us what row to look at\n", + " col = loc % self.dim_size # we want the remainder to tell us what index in that row to look at\n", + "\n", + " if board[row][col] == '*':\n", + " # this means we've actually planted a bomb there already so keep going\n", + " continue\n", + "\n", + " board[row][col] = '*' # plant the bomb\n", + " bombs_planted += 1\n", + "\n", + " return board\n", + "\n", + " def assign_values_to_board(self):\n", + " # now that we have the bombs planted, let's assign a number 0-8 for all the empty spaces, which\n", + " # represents how many neighboring bombs there are. we can precompute these and it'll save us some\n", + " # effort checking what's around the board later on :)\n", + " for r in range(self.dim_size):\n", + " for c in range(self.dim_size):\n", + " if self.board[r][c] == '*':\n", + " # if this is already a bomb, we don't want to calculate anything\n", + " continue\n", + " self.board[r][c] = self.get_num_neighboring_bombs(r, c)\n", + "\n", + " def get_num_neighboring_bombs(self, row, col):\n", + " # let's iterate through each of the neighboring positions and sum number of bombs\n", + " # top left: (row-1, col-1)\n", + " # top middle: (row-1, col)\n", + " # top right: (row-1, col+1)\n", + " # left: (row, col-1)\n", + " # right: (row, col+1)\n", + " # bottom left: (row+1, col-1)\n", + " # bottom middle: (row+1, col)\n", + " # bottom right: (row+1, col+1)\n", + "\n", + " # make sure to not go out of bounds!\n", + "\n", + " num_neighboring_bombs = 0\n", + " for r in range(max(0, row-1), min(self.dim_size-1, row+1)+1):\n", + " for c in range(max(0, col-1), min(self.dim_size-1, col+1)+1):\n", + " if r == row and c == col:\n", + " # our original location, don't check\n", + " continue\n", + " if self.board[r][c] == '*':\n", + " num_neighboring_bombs += 1\n", + "\n", + " return num_neighboring_bombs\n", + "\n", + " def dig(self, row, col):\n", + " # dig at that location!\n", + " # return True if successful dig, False if bomb dug\n", + "\n", + " # a few scenarios:\n", + " # hit a bomb -> game over\n", + " # dig at location with neighboring bombs -> finish dig\n", + " # dig at location with no neighboring bombs -> recursively dig neighbors!\n", + "\n", + " self.dug.add((row, col)) # keep track that we dug here\n", + "\n", + " if self.board[row][col] == '*':\n", + " return False\n", + " elif self.board[row][col] > 0:\n", + " return True\n", + "\n", + " # self.board[row][col] == 0\n", + " for r in range(max(0, row-1), min(self.dim_size-1, row+1)+1):\n", + " for c in range(max(0, col-1), min(self.dim_size-1, col+1)+1):\n", + " if (r, c) in self.dug:\n", + " continue # don't dig where you've already dug\n", + " self.dig(r, c)\n", + "\n", + " # if our initial dig didn't hit a bomb, we *shouldn't* hit a bomb here\n", + " return True\n", + "\n", + " def __str__(self):\n", + " # this is a magic function where if you call print on this object,\n", + " # it'll print out what this function returns!\n", + " # return a string that shows the board to the player\n", + "\n", + " # first let's create a new array that represents what the user would see\n", + " visible_board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)]\n", + " for row in range(self.dim_size):\n", + " for col in range(self.dim_size):\n", + " if (row,col) in self.dug:\n", + " visible_board[row][col] = str(self.board[row][col])\n", + " else:\n", + " visible_board[row][col] = ' '\n", + " \n", + " # put this together in a string\n", + " string_rep = ''\n", + " # get max column widths for printing\n", + " widths = []\n", + " for idx in range(self.dim_size):\n", + " columns = map(lambda x: x[idx], visible_board)\n", + " widths.append(\n", + " len(\n", + " max(columns, key = len)\n", + " )\n", + " )\n", + "\n", + " # print the csv strings\n", + " indices = [i for i in range(self.dim_size)]\n", + " indices_row = ' '\n", + " cells = []\n", + " for idx, col in enumerate(indices):\n", + " format = '%-' + str(widths[idx]) + \"s\"\n", + " cells.append(format % (col))\n", + " indices_row += ' '.join(cells)\n", + " indices_row += ' \\n'\n", + " \n", + " for i in range(len(visible_board)):\n", + " row = visible_board[i]\n", + " string_rep += f'{i} |'\n", + " cells = []\n", + " for idx, col in enumerate(row):\n", + " format = '%-' + str(widths[idx]) + \"s\"\n", + " cells.append(format % (col))\n", + " string_rep += ' |'.join(cells)\n", + " string_rep += ' |\\n'\n", + "\n", + " str_len = int(len(string_rep) / self.dim_size)\n", + " string_rep = indices_row + '-'*str_len + '\\n' + string_rep + '-'*str_len\n", + "\n", + " return string_rep\n", + "\n", + "# play the game\n", + "def play(dim_size=10, num_bombs=10):\n", + " # Step 1: create the board and plant the bombs\n", + " board = Board(dim_size, num_bombs)\n", + "\n", + " # Step 2: show the user the board and ask for where they want to dig\n", + " # Step 3a: if location is a bomb, show game over message\n", + " # Step 3b: if location is not a bomb, dig recursively until each square is at least\n", + " # next to a bomb\n", + " # Step 4: repeat steps 2 and 3a/b until there are no more places to dig -> VICTORY!\n", + " safe = True \n", + "\n", + " while len(board.dug) < board.dim_size ** 2 - num_bombs:\n", + " print(board)\n", + " # 0,0 or 0, 0 or 0, 0\n", + " user_input = re.split(',(\\\\s)*', input(\"Where would you like to dig? Input as row,col: \")) # '0, 3'\n", + " row, col = int(user_input[0]), int(user_input[-1])\n", + " if row < 0 or row >= board.dim_size or col < 0 or col >= dim_size:\n", + " print(\"Invalid location. Try again.\")\n", + " continue\n", + "\n", + " # if it's valid, we dig\n", + " safe = board.dig(row, col)\n", + " if not safe:\n", + " # dug a bomb ahhhhhhh\n", + " break # (game over rip)\n", + "\n", + " # 2 ways to end loop, lets check which one\n", + " if safe:\n", + " print(\"CONGRATULATIONS!!!! YOU ARE VICTORIOUS!\")\n", + " else:\n", + " print(\"SORRY GAME OVER :(\")\n", + " # let's reveal the whole board!\n", + " board.dug = [(r,c) for r in range(board.dim_size) for c in range(board.dim_size)]\n", + " print(board)\n", + "\n", + "if __name__ == '__main__': # good practice :)\n", + " play()" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "text": [ + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 | | | | | | | | | | |\n", + "1 | | | | | | | | | | |\n", + "2 | | | | | | | | | | |\n", + "3 | | | | | | | | | | |\n", + "4 | | | | | | | | | | |\n", + "5 | | | | | | | | | | |\n", + "6 | | | | | | | | | | |\n", + "7 | | | | | | | | | | |\n", + "8 | | | | | | | | | | |\n", + "9 | | | | | | | | | | |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 0,0\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 | | | | | | | | | |\n", + "1 | | | | | | | | | | |\n", + "2 | | | | | | | | | | |\n", + "3 | | | | | | | | | | |\n", + "4 | | | | | | | | | | |\n", + "5 | | | | | | | | | | |\n", + "6 | | | | | | | | | | |\n", + "7 | | | | | | | | | | |\n", + "8 | | | | | | | | | | |\n", + "9 | | | | | | | | | | |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 0,1\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 |1 | | | | | | | | |\n", + "1 | | | | | | | | | | |\n", + "2 | | | | | | | | | | |\n", + "3 | | | | | | | | | | |\n", + "4 | | | | | | | | | | |\n", + "5 | | | | | | | | | | |\n", + "6 | | | | | | | | | | |\n", + "7 | | | | | | | | | | |\n", + "8 | | | | | | | | | | |\n", + "9 | | | | | | | | | | |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 9,9\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 |1 | | | | | | | | |\n", + "1 | | | | | | | |1 |1 |1 |\n", + "2 | | | | | | | |1 |0 |0 |\n", + "3 | | | | | | | |1 |0 |0 |\n", + "4 | | | | | | |2 |1 |0 |0 |\n", + "5 | | | | | | |2 |0 |0 |0 |\n", + "6 | | | | | | |2 |0 |0 |0 |\n", + "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", + "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 3,3\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 |1 |1 |0 |0 |0 |0 |1 | | |\n", + "1 | | |1 |0 |0 |0 |0 |1 |1 |1 |\n", + "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", + "3 |0 |0 |0 |0 |0 |1 | |1 |0 |0 |\n", + "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", + "5 | | |2 |1 |2 | |2 |0 |0 |0 |\n", + "6 | | | | | | |2 |0 |0 |0 |\n", + "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", + "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 0,9\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 |1 |1 |0 |0 |0 |0 |1 | |1 |\n", + "1 | | |1 |0 |0 |0 |0 |1 |1 |1 |\n", + "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", + "3 |0 |0 |0 |0 |0 |1 | |1 |0 |0 |\n", + "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", + "5 | | |2 |1 |2 | |2 |0 |0 |0 |\n", + "6 | | | | | | |2 |0 |0 |0 |\n", + "7 | | |1 |1 |1 |1 |1 |0 |0 |0 |\n", + "8 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "9 | | |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "----------------------------------\n", + "Where would you like to dig? Input as row,col: 9,0\n", + "SORRY GAME OVER :(\n", + " 0 1 2 3 4 5 6 7 8 9 \n", + "----------------------------------\n", + "0 |1 |1 |1 |0 |0 |0 |0 |1 |* |1 |\n", + "1 |1 |* |1 |0 |0 |0 |0 |1 |1 |1 |\n", + "2 |1 |1 |1 |0 |0 |1 |1 |1 |0 |0 |\n", + "3 |0 |0 |0 |0 |0 |1 |* |1 |0 |0 |\n", + "4 |1 |1 |1 |0 |1 |2 |2 |1 |0 |0 |\n", + "5 |2 |* |2 |1 |2 |* |2 |0 |0 |0 |\n", + "6 |* |3 |* |1 |2 |* |2 |0 |0 |0 |\n", + "7 |1 |2 |1 |1 |1 |1 |1 |0 |0 |0 |\n", + "8 |2 |2 |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "9 |* |* |1 |0 |0 |0 |0 |0 |0 |0 |\n", + "----------------------------------\n" + ] + } + ], + "id": "1d0237c9" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "93ea2967" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "import random\n", - "import re\n", - "\n", - "# lets create a board object to represent the minesweeper game\n", - "# this is so that we can just say \"create a new board object\", or\n", - "# \"dig here\", or \"render this game for this object\"\n", - "class Board:\n", - " def __init__(self, dim_size, num_bombs):\n", - " # let's keep track of these parameters. they'll be helpful later\n", - " self.dim_size = dim_size\n", - " self.num_bombs = num_bombs\n", - "\n", - " # let's create the board\n", - " # helper function!\n", - " self.board = self.make_new_board() # plant the bombs\n", - " self.assign_values_to_board()\n", - "\n", - " # initialize a set to keep track of which locations we've uncovered\n", - " # we'll save (row,col) tuples into this set \n", - " self.dug = set() # if we dig at 0, 0, then self.dug = {(0,0)}\n", - "\n", - " def make_new_board(self):\n", - " # construct a new board based on the dim size and num bombs\n", - " # we should construct the list of lists here (or whatever representation you prefer,\n", - " # but since we have a 2-D board, list of lists is most natural)\n", - "\n", - " # generate a new board\n", - " board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)]\n", - " # this creates an array like this:\n", - " # [[None, None, ..., None],\n", - " # [None, None, ..., None],\n", - " # [... ],\n", - " # [None, None, ..., None]]\n", - " # we can see how this represents a board!\n", - "\n", - " # plant the bombs\n", - " bombs_planted = 0\n", - " while bombs_planted < self.num_bombs:\n", - " loc = random.randint(0, self.dim_size**2 - 1) # return a random integer N such that a <= N <= b\n", - " row = loc // self.dim_size # we want the number of times dim_size goes into loc to tell us what row to look at\n", - " col = loc % self.dim_size # we want the remainder to tell us what index in that row to look at\n", - "\n", - " if board[row][col] == '*':\n", - " # this means we've actually planted a bomb there already so keep going\n", - " continue\n", - "\n", - " board[row][col] = '*' # plant the bomb\n", - " bombs_planted += 1\n", - "\n", - " return board\n", - "\n", - " def assign_values_to_board(self):\n", - " # now that we have the bombs planted, let's assign a number 0-8 for all the empty spaces, which\n", - " # represents how many neighboring bombs there are. we can precompute these and it'll save us some\n", - " # effort checking what's around the board later on :)\n", - " for r in range(self.dim_size):\n", - " for c in range(self.dim_size):\n", - " if self.board[r][c] == '*':\n", - " # if this is already a bomb, we don't want to calculate anything\n", - " continue\n", - " self.board[r][c] = self.get_num_neighboring_bombs(r, c)\n", - "\n", - " def get_num_neighboring_bombs(self, row, col):\n", - " # let's iterate through each of the neighboring positions and sum number of bombs\n", - " # top left: (row-1, col-1)\n", - " # top middle: (row-1, col)\n", - " # top right: (row-1, col+1)\n", - " # left: (row, col-1)\n", - " # right: (row, col+1)\n", - " # bottom left: (row+1, col-1)\n", - " # bottom middle: (row+1, col)\n", - " # bottom right: (row+1, col+1)\n", - "\n", - " # make sure to not go out of bounds!\n", - "\n", - " num_neighboring_bombs = 0\n", - " for r in range(max(0, row-1), min(self.dim_size-1, row+1)+1):\n", - " for c in range(max(0, col-1), min(self.dim_size-1, col+1)+1):\n", - " if r == row and c == col:\n", - " # our original location, don't check\n", - " continue\n", - " if self.board[r][c] == '*':\n", - " num_neighboring_bombs += 1\n", - "\n", - " return num_neighboring_bombs\n", - "\n", - " def dig(self, row, col):\n", - " # dig at that location!\n", - " # return True if successful dig, False if bomb dug\n", - "\n", - " # a few scenarios:\n", - " # hit a bomb -> game over\n", - " # dig at location with neighboring bombs -> finish dig\n", - " # dig at location with no neighboring bombs -> recursively dig neighbors!\n", - "\n", - " self.dug.add((row, col)) # keep track that we dug here\n", - "\n", - " if self.board[row][col] == '*':\n", - " return False\n", - " elif self.board[row][col] > 0:\n", - " return True\n", - "\n", - " # self.board[row][col] == 0\n", - " for r in range(max(0, row-1), min(self.dim_size-1, row+1)+1):\n", - " for c in range(max(0, col-1), min(self.dim_size-1, col+1)+1):\n", - " if (r, c) in self.dug:\n", - " continue # don't dig where you've already dug\n", - " self.dig(r, c)\n", - "\n", - " # if our initial dig didn't hit a bomb, we *shouldn't* hit a bomb here\n", - " return True\n", - "\n", - " def __str__(self):\n", - " # this is a magic function where if you call print on this object,\n", - " # it'll print out what this function returns!\n", - " # return a string that shows the board to the player\n", - "\n", - " # first let's create a new array that represents what the user would see\n", - " visible_board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)]\n", - " for row in range(self.dim_size):\n", - " for col in range(self.dim_size):\n", - " if (row,col) in self.dug:\n", - " visible_board[row][col] = str(self.board[row][col])\n", - " else:\n", - " visible_board[row][col] = ' '\n", - " \n", - " # put this together in a string\n", - " string_rep = ''\n", - " # get max column widths for printing\n", - " widths = []\n", - " for idx in range(self.dim_size):\n", - " columns = map(lambda x: x[idx], visible_board)\n", - " widths.append(\n", - " len(\n", - " max(columns, key = len)\n", - " )\n", - " )\n", - "\n", - " # print the csv strings\n", - " indices = [i for i in range(self.dim_size)]\n", - " indices_row = ' '\n", - " cells = []\n", - " for idx, col in enumerate(indices):\n", - " format = '%-' + str(widths[idx]) + \"s\"\n", - " cells.append(format % (col))\n", - " indices_row += ' '.join(cells)\n", - " indices_row += ' \\n'\n", - " \n", - " for i in range(len(visible_board)):\n", - " row = visible_board[i]\n", - " string_rep += f'{i} |'\n", - " cells = []\n", - " for idx, col in enumerate(row):\n", - " format = '%-' + str(widths[idx]) + \"s\"\n", - " cells.append(format % (col))\n", - " string_rep += ' |'.join(cells)\n", - " string_rep += ' |\\n'\n", - "\n", - " str_len = int(len(string_rep) / self.dim_size)\n", - " string_rep = indices_row + '-'*str_len + '\\n' + string_rep + '-'*str_len\n", - "\n", - " return string_rep\n", - "\n", - "# play the game\n", - "def play(dim_size=10, num_bombs=10):\n", - " # Step 1: create the board and plant the bombs\n", - " board = Board(dim_size, num_bombs)\n", - "\n", - " # Step 2: show the user the board and ask for where they want to dig\n", - " # Step 3a: if location is a bomb, show game over message\n", - " # Step 3b: if location is not a bomb, dig recursively until each square is at least\n", - " # next to a bomb\n", - " # Step 4: repeat steps 2 and 3a/b until there are no more places to dig -> VICTORY!\n", - " safe = True \n", - "\n", - " while len(board.dug) < board.dim_size ** 2 - num_bombs:\n", - " print(board)\n", - " # 0,0 or 0, 0 or 0, 0\n", - " user_input = re.split(',(\\\\s)*', input(\"Where would you like to dig? Input as row,col: \")) # '0, 3'\n", - " row, col = int(user_input[0]), int(user_input[-1])\n", - " if row < 0 or row >= board.dim_size or col < 0 or col >= dim_size:\n", - " print(\"Invalid location. Try again.\")\n", - " continue\n", - "\n", - " # if it's valid, we dig\n", - " safe = board.dig(row, col)\n", - " if not safe:\n", - " # dug a bomb ahhhhhhh\n", - " break # (game over rip)\n", - "\n", - " # 2 ways to end loop, lets check which one\n", - " if safe:\n", - " print(\"CONGRATULATIONS!!!! YOU ARE VICTORIOUS!\")\n", - " else:\n", - " print(\"SORRY GAME OVER :(\")\n", - " # let's reveal the whole board!\n", - " board.dug = [(r,c) for r in range(board.dim_size) for c in range(board.dim_size)]\n", - " print(board)\n", - "\n", - "if __name__ == '__main__': # good practice :)\n", - " play()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "93ea2967", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/README.md b/README.md index 4a1b6cd..0ed3889 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,158 @@ -# Learn-Python -freeCodeCamp +# Learn-Python Mini Projects -https://www.youtube.com/watch?v=8ext9G7xspg +A collection of beginner-friendly Python mini projects that is being upgraded from notebook proofs-of-concept into a structured, testable package. + +## Quick Start + +For full documentation navigation, start with `docs/README.md`. + +### Prerequisites +- Python 3.11+ +- `pip` + +### Setup +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +``` + +### Run TicTacToe (Phase 1 feature) +```bash +PYTHONPATH=src python -m mini_projects.cli tictactoe --difficulty hard +``` + +### Run Expansion Capstone Demo +```bash +PYTHONPATH=src python -m mini_projects.cli curriculum-demo +``` + +### Run World Bank Real-Data Sync +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-sync --countries US CA GB DE JP --indicators NY.GDP.MKTP.KD.ZG FP.CPI.TOTL.ZG SL.UEM.TOTL.ZS +``` + +### Check World Bank Freshness Status +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-status +``` + +Available difficulty levels: +- `easy`: random legal moves +- `medium`: mixed random + shallow minimax +- `hard`: full minimax with alpha-beta pruning + +## Project Structure + +```text +Learn-Python/ + src/ + mini_projects/ + cli.py + features/ + advanced_python/ + dsa/ + async_concurrency/ + backend_api/ + data_stack/ + dashboard/ + big_data/ + capstone/ + tictactoe/ + domain/ + application/ + presentation/ + tests/ + features/ + tictactoe/ + .github/workflows/ci.yml + requirements.txt + requirements-dev.txt +``` + +## Development + +### Run tests +```bash +PYTHONPATH=src pytest -q +``` + +### CI +GitHub Actions runs test checks on every push and pull request through `.github/workflows/ci.yml`. + +## Current Modules + +- **TicTacToe (modernized package):** `src/mini_projects/features/tictactoe` with CLI + alpha-beta AI difficulty levels. +- **Advanced Python:** decorators, generators, context managers, protocols, dataclasses. +- **DSA Deepening:** trie, union-find, Dijkstra shortest path, LIS dynamic programming pattern. +- **Async/Concurrency:** retry, bounded concurrency, timeout fanout, model-selection helper. +- **Backend API:** FastAPI notes service with validation and SQLite adapter. +- **Data Stack:** normalized indicator schema and KPI/category analysis workflow. +- **Dashboard Building:** payload builder for KPI cards and trend/category views from indicator data. +- **Big Data Analysis:** chunked processing and scaling strategy selector. +- **Capstone Integration:** World Bank ingestion + cache freshness + dashboard + big-data strategy in one runnable flow. + +## World Bank Data Pipeline + +- Source: World Bank Indicators API v2 (free and open, no API key). +- Adapter path: `src/mini_projects/features/capstone/infrastructure/world_bank_client.py` +- Cache/freshness path: `src/mini_projects/features/capstone/infrastructure/cache_store.py` +- Orchestration path: `src/mini_projects/features/capstone/application/pipeline.py` +- API endpoints: + - `POST /world-bank/refresh` + - `GET /world-bank/latest` + - `GET /world-bank/status` +- Runbook index: `docs/pipelines/world-bank/overview.md` + +## Python Expansion Curriculum + +- Curriculum guide: `docs/curriculum/overview.md` +- New tests live under `tests/features/` for each added module. +- This expansion demonstrates language depth, algorithms, async systems, backend services, data analysis, dashboard thinking, and big-data scaling decisions. + +## Notebook Projects (Upgraded In Place) + +- `Hangman.ipynb`: categories, hint recommendation, multiplayer helper, JSON score persistence. +- `Minesweeper.ipynb`: flags, timer, SQLite leaderboard, solver-hint mode. +- `sudoku.ipynb`: constraint propagation + backtracking + puzzle generation. +- `Rock Paper Scissors.ipynb`: input normalization, best-of rounds, adaptive opponent model. +- `Binary Search.ipynb`: expanded to visualizer-style algorithms notebook (binary search, BFS, DFS, quicksort, mergesort). +- `Guess the number.ipynb`: robust validation, attempts tracking, reverse mode. +- `madlib.ipynb`: multi-template system with validation and deterministic selection support. +- `Simple Calculator.ipynb`: expression parser, safe errors, history session runner. +- `Find the month.ipynb`: normalized lookup, fallback handling, fuzzy suggestion helper. + +## AI/ML-Enabled Notebook Features + +- Hangman letter recommendation. +- Minesweeper safe-cell hinting. +- Sudoku step ranking via constraint-first candidate flow. +- Rock Paper Scissors Markov-style opponent prediction. +- Binary Search notebook includes empirical timing trend comparisons. +- Guess the Number adaptive mode helper via binary-search strategy. + +## Usage Notes + +- Notebooks now follow a consistent pattern: intro -> helpers -> runner -> assertions/demo. +- New code should be added under `src/mini_projects` and verified with tests in `tests/`. +- Some interactive notebook runners require user input and are intended to be executed cell-by-cell. + +## Remaining Scripts Pending Migration + +- `compose.py`, `compose_template.py`, `graph.py`, `graph_template.py` +- `image.py`, `transform.py`, `transform_empty.py`, `png.py` +- `lyrics.py`, `words.py` + +Modernization notes and contracts for these scripts are documented in `docs/migration/script-modernization.md`. + +## Screenshots + +Place screenshots or GIFs in a future `docs/assets/` folder and reference them here. + +## Roadmap + +- Minesweeper GUI with timer, flags, SQLite leaderboard, and solver mode. +- Sudoku with constraint propagation, visualization, and puzzle generation. +- Hangman with categories, persistent scores, and multiplayer mode. +- Algorithms visualizer (binary search, BFS, DFS, quicksort, mergesort, and more). +- Optional TicTacToe Monte Carlo Tree Search (MCTS) mode. diff --git a/Rock Paper Scissors.ipynb b/Rock Paper Scissors.ipynb index 6d16393..22db4c1 100644 --- a/Rock Paper Scissors.ipynb +++ b/Rock Paper Scissors.ipynb @@ -1,81 +1,147 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 13, - "id": "31a96691", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - " What is your choice? 'r' for rock, 'p' for paper, 's' for scissors\n", - "r\n", - "It's a tie\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Rock Paper Scissors\n", + "\n", + "Goal: multi-round gameplay with adaptive opponent logic.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Gameplay runner\n", + "4. Quick verification checks" + ], + "id": "d6712221" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Rock Paper Scissors with normalization, score tracking, and adaptive AI.\"\"\"\n", + "\n", + "import random\n", + "from collections import defaultdict\n", + "\n", + "BEATS = {\"r\": \"s\", \"p\": \"r\", \"s\": \"p\"}\n", + "FULL_TO_SHORT = {\"rock\": \"r\", \"paper\": \"p\", \"scissors\": \"s\", \"r\": \"r\", \"p\": \"p\", \"s\": \"s\"}\n", + "\n", + "\n", + "def normalize_choice(raw: str) -> str:\n", + " key = raw.strip().lower()\n", + " if key not in FULL_TO_SHORT:\n", + " raise ValueError(\"Use rock/paper/scissors or r/p/s\")\n", + " return FULL_TO_SHORT[key]\n", + "\n", + "\n", + "def determine_outcome(user: str, computer: str) -> str:\n", + " if user == computer:\n", + " return \"tie\"\n", + " return \"win\" if BEATS[user] == computer else \"loss\"\n", + "\n", + "\n", + "def predict_user_move(history: list[str]) -> str | None:\n", + " if len(history) < 2:\n", + " return None\n", + " transitions: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))\n", + " for a, b in zip(history[:-1], history[1:]):\n", + " transitions[a][b] += 1\n", + " last = history[-1]\n", + " if not transitions[last]:\n", + " return None\n", + " return max(transitions[last], key=transitions[last].get)\n", + "\n", + "\n", + "def computer_choice(rng: random.Random, user_history: list[str]) -> str:\n", + " predicted = predict_user_move(user_history)\n", + " if predicted is None:\n", + " return rng.choice([\"r\", \"p\", \"s\"])\n", + " # Choose move that beats predicted user move.\n", + " for move, beats in BEATS.items():\n", + " if beats == predicted:\n", + " return move\n", + " return rng.choice([\"r\", \"p\", \"s\"])\n", + "\n", + "\n", + "def play_best_of(rounds: int = 3, rng_seed: int = 0) -> dict[str, int]:\n", + " rng = random.Random(rng_seed)\n", + " score = {\"user\": 0, \"computer\": 0, \"tie\": 0}\n", + " user_history: list[str] = []\n", + "\n", + " while score[\"user\"] + score[\"computer\"] < rounds:\n", + " raw = input(\"Choice (rock/paper/scissors): \")\n", + " try:\n", + " user = normalize_choice(raw)\n", + " except ValueError as exc:\n", + " print(exc)\n", + " continue\n", + " comp = computer_choice(rng, user_history)\n", + " user_history.append(user)\n", + " outcome = determine_outcome(user, comp)\n", + " score[\"tie\" if outcome == \"tie\" else (\"user\" if outcome == \"win\" else \"computer\")] += 1\n", + " print(f\"Computer chose {comp}. Outcome: {outcome}\")\n", + "\n", + " return score\n", + "\n", + "\n", + "# Quick verification checks\n", + "assert normalize_choice(\"Rock\") == \"r\"\n", + "assert determine_outcome(\"r\", \"s\") == \"win\"\n", + "assert computer_choice(random.Random(1), [\"r\", \"p\", \"r\"]) in {\"r\", \"p\", \"s\"}\n", + "\n", + "print(\"Demo outcome check:\", determine_outcome(\"p\", \"r\"))" + ], + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "text": [ + " What is your choice? 'r' for rock, 'p' for paper, 's' for scissors\n", + "r\n", + "It's a tie\n" + ] + } + ], + "id": "31a96691" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "c9e5fe33" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "b8f1875a" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "import random\n", - "\n", - "def play():\n", - " user = input (\" What is your choice? 'r' for rock, 'p' for paper, 's' for scissors\\n\")\n", - " computer = random.choice(['r', 'p', 's'])\n", - " \n", - " if user == computer:\n", - " return 'It\\'s a tie'\n", - " \n", - " if is_win(user, computer):\n", - " return 'You won!'\n", - " \n", - " return 'You lost!'\n", - "\n", - "\n", - "def is_win(player, opponent):\n", - " if (player == 'r' and opponent == 's') or (player == 's' and opponent == 'p') \\\n", - " or (player == 'p' and opponent == 'r'):\n", - " return True\n", - "\n", - "print(play())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c9e5fe33", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8f1875a", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/Simple Calculator.ipynb b/Simple Calculator.ipynb index 24da755..40d0d66 100644 --- a/Simple Calculator.ipynb +++ b/Simple Calculator.ipynb @@ -1,67 +1,136 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 3, - "id": "a26aeb78", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Enter first number: 5\n", - "Enter operator: +\n", - "Enter second number: 3\n", - "8.0\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simple Calculator\n", + "\n", + "Goal: parse and evaluate expressions safely with clear errors.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Session runner\n", + "4. Quick verification checks and demo" + ], + "id": "d4a663b8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Simple calculator with parser, history, and safe error handling.\"\"\"\n", + "\n", + "from dataclasses import dataclass\n", + "\n", + "\n", + "OPERATORS = {\n", + " \"+\": lambda a, b: a + b,\n", + " \"-\": lambda a, b: a - b,\n", + " \"*\": lambda a, b: a * b,\n", + " \"/\": lambda a, b: a / b,\n", + " \"pow\": lambda a, b: a**b,\n", + " \"%\": lambda a, b: a % b,\n", + "}\n", + "\n", + "\n", + "@dataclass\n", + "class CalcRecord:\n", + " expression: str\n", + " result: float\n", + "\n", + "\n", + "def parse_expression(text: str) -> tuple[float, str, float]:\n", + " parts = text.strip().split()\n", + " if len(parts) != 3:\n", + " raise ValueError(\"Use format: \")\n", + " left, op, right = parts\n", + " if op not in OPERATORS:\n", + " raise ValueError(f\"Unsupported operator: {op}\")\n", + " return float(left), op, float(right)\n", + "\n", + "\n", + "def calculate(left: float, op: str, right: float) -> float:\n", + " if op == \"/\" and right == 0:\n", + " raise ZeroDivisionError(\"Division by zero is not allowed.\")\n", + " return OPERATORS[op](left, right)\n", + "\n", + "\n", + "def run_calculator_session(input_fn=input, output_fn=print) -> list[CalcRecord]:\n", + " history: list[CalcRecord] = []\n", + " output_fn(\"Enter expressions like: 2 + 3 (or 'quit').\")\n", + " while True:\n", + " raw = input_fn(\"> \").strip()\n", + " if raw.lower() == \"quit\":\n", + " break\n", + " try:\n", + " left, op, right = parse_expression(raw)\n", + " result = calculate(left, op, right)\n", + " history.append(CalcRecord(expression=raw, result=result))\n", + " output_fn(f\"= {result}\")\n", + " except Exception as exc:\n", + " output_fn(f\"Error: {exc}\")\n", + " return history\n", + "\n", + "\n", + "# Quick verification checks\n", + "assert parse_expression(\"2 + 3\") == (2.0, \"+\", 3.0)\n", + "assert calculate(2, \"pow\", 3) == 8\n", + "try:\n", + " calculate(3, \"/\", 0)\n", + " raise AssertionError(\"Expected division by zero\")\n", + "except ZeroDivisionError:\n", + " pass\n", + "\n", + "\n", + "demo = [\"2 + 3\", \"5 pow 2\", \"7 / 0\", \"quit\"]\n", + "it = iter(demo)\n", + "history = run_calculator_session(input_fn=lambda _: next(it), output_fn=print)\n", + "print(\"History size:\", len(history))" + ], + "execution_count": 3, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Enter first number: 5\n", + "Enter operator: +\n", + "Enter second number: 3\n", + "8.0\n" + ] + } + ], + "id": "a26aeb78" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### " + ], + "id": "3cf8629c" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "num1= float(input(\"Enter first number: \"))\n", - "op= input(\"Enter operator: \")\n", - "num2= float(input(\"Enter second number: \"))\n", - "\n", - "if op == \"+\":\n", - " print(num1 + num2)\n", - "elif op == \"-\":\n", - " print(num1 - num2)\n", - "elif op == \"/\":\n", - " print(num1 / num2)\n", - "elif op == \"*\":\n", - " print(num1 * num2)\n", - "else:\n", - " print(\"Invalid operator\")" - ] - }, - { - "cell_type": "markdown", - "id": "3cf8629c", - "metadata": {}, - "source": [ - "### " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/compose.py b/compose.py index 0d23979..18a90d4 100644 --- a/compose.py +++ b/compose.py @@ -54,7 +54,7 @@ def compose(g, words, length=50): def main(): - words = get_words_from_text('texts/hp_sorcerer_stone.txt') + words = get_words_from_text('hp_socerer_stone.txt') # for song in os.listdir('songs/{}'.format(artist)): # if song == '.DS_Store': diff --git a/compose_template.py b/compose_template.py index b6d4eeb..cd86b98 100644 --- a/compose_template.py +++ b/compose_template.py @@ -7,16 +7,38 @@ from graph import Graph, Vertex def get_words_from_text(text_path): - pass + with open(text_path, "rb") as file: + text = file.read().decode("utf-8") + text = " ".join(text.split()) + text = text.lower() + text = text.translate(str.maketrans("", "", string.punctuation)) + words = text.split() + return words[:1000] def make_graph(words): - pass + g = Graph() + prev_word = None + for word in words: + word_vertex = g.get_vertex(word) + if prev_word: + prev_word.increment_edge(word_vertex) + prev_word = word_vertex + g.generate_probability_mappings() + return g def compose(g, words, length=50): - pass + composition = [] + current = g.get_vertex(random.choice(words)) + for _ in range(length): + composition.append(current.value) + current = g.get_next_word(current) + return composition def main(): - pass + words = get_words_from_text("hp_socerer_stone.txt") + graph = make_graph(words) + composition = compose(graph, words, 100) + print(" ".join(composition)) if __name__ == '__main__': diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..37f586e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,35 @@ +# Documentation Index + +This repository documentation is organized by domain so each area stays focused and easy to maintain. + +## Getting Started + +- `getting-started/quickstart.md`: local setup, commands, and first-run checks. + +## Curriculum + +- `curriculum/overview.md`: full curriculum map and learning progression. +- `curriculum/modules/core_and_algorithms.md`: language + DSA tracks. +- `curriculum/modules/systems_and_data.md`: async, APIs, data, dashboard, and capstone tracks. + +## Pipelines + +- `pipelines/world-bank/overview.md`: architecture, data flow, and schema. +- `pipelines/world-bank/api-contracts.md`: endpoint and CLI contracts. +- `pipelines/world-bank/operations.md`: refresh policy, cache behavior, and troubleshooting. + +## Migration + +- `migration/script-modernization.md`: remaining script migration contracts and next steps. + +## Testing + +- `testing/verification.md`: full and targeted verification commands. + +## Legacy Path Compatibility + +These files are retained as compatibility entrypoints and now point to the reorganized docs: + +- `python_expansion_curriculum.md` +- `world_bank_data_pipeline.md` +- `script_modernization_guide.md` diff --git a/docs/curriculum/modules/core_and_algorithms.md b/docs/curriculum/modules/core_and_algorithms.md new file mode 100644 index 0000000..440fe96 --- /dev/null +++ b/docs/curriculum/modules/core_and_algorithms.md @@ -0,0 +1,35 @@ +# Core Language and Algorithms Track + +## Advanced Python Features + +Implementation path: +- `src/mini_projects/features/advanced_python/application/patterns.py` + +Learning goals: +- decorators and retry wrappers +- generators and memory-safe iteration +- context manager usage +- `dataclass` and `Protocol` design patterns + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/advanced_python +``` + +## Data Structures and Algorithms + +Implementation path: +- `src/mini_projects/features/dsa/application/algorithms.py` + +Learning goals: +- trie insertion/search/prefix matching +- union-find connectivity +- Dijkstra shortest path +- LIS dynamic-programming pattern + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/dsa +``` diff --git a/docs/curriculum/modules/systems_and_data.md b/docs/curriculum/modules/systems_and_data.md new file mode 100644 index 0000000..47d220a --- /dev/null +++ b/docs/curriculum/modules/systems_and_data.md @@ -0,0 +1,55 @@ +# Systems and Data Track + +## Async and Concurrency + +Implementation path: +- `src/mini_projects/features/async_concurrency/application/pipeline.py` + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/async_concurrency +``` + +## Backend API + +Implementation paths: +- `src/mini_projects/features/backend_api/presentation/app.py` +- `src/mini_projects/features/backend_api/infrastructure/repository.py` + +Includes refresh/latest/status API endpoints for World Bank data. + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/backend_api +``` + +## Data Analysis and Dashboard + +Implementation paths: +- `src/mini_projects/features/data_stack/application/analysis.py` +- `src/mini_projects/features/dashboard/application/dashboard.py` +- `src/mini_projects/features/big_data/application/chunked_processing.py` + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/data_stack tests/features/big_data +``` + +## Capstone Integration + +Implementation path: +- `src/mini_projects/features/capstone/application/pipeline.py` + +World Bank pipeline details: +- `../../pipelines/world-bank/overview.md` + +Verification: + +```bash +PYTHONPATH=src pytest -q tests/features/capstone +PYTHONPATH=src python -m mini_projects.cli world-bank-sync --countries US CA GB DE JP --indicators NY.GDP.MKTP.KD.ZG FP.CPI.TOTL.ZG SL.UEM.TOTL.ZS +PYTHONPATH=src python -m mini_projects.cli world-bank-status +``` diff --git a/docs/curriculum/overview.md b/docs/curriculum/overview.md new file mode 100644 index 0000000..ee739d7 --- /dev/null +++ b/docs/curriculum/overview.md @@ -0,0 +1,25 @@ +# Python Expansion Curriculum Overview + +This curriculum extends the notebook baseline into production-style Python skills across language depth, algorithms, systems, APIs, data engineering, and analytics. + +## Module Map + +1. Advanced Python features +2. Data structures and algorithms +3. Async and concurrency +4. Backend API development +5. Data analysis +6. Dashboard payload design +7. Big-data analysis foundations +8. Integration capstone with World Bank real-data flow + +## Track Documents + +- `modules/core_and_algorithms.md` +- `modules/systems_and_data.md` + +## End-to-End Verification + +```bash +PYTHONPATH=src pytest -q +``` diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..0b91495 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,46 @@ +# Quickstart + +## Prerequisites + +- Python 3.11+ +- `pip` + +## Environment Setup + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +``` + +## Run Key Commands + +Run TicTacToe: + +```bash +PYTHONPATH=src python -m mini_projects.cli tictactoe --difficulty hard +``` + +Run curriculum demo: + +```bash +PYTHONPATH=src python -m mini_projects.cli curriculum-demo +``` + +Run real-data World Bank sync: + +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-sync --countries US CA GB DE JP --indicators NY.GDP.MKTP.KD.ZG FP.CPI.TOTL.ZG SL.UEM.TOTL.ZS +``` + +Check World Bank freshness: + +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-status +``` + +## Verify Project Health + +```bash +PYTHONPATH=src pytest -q +``` diff --git a/docs/migration/script-modernization.md b/docs/migration/script-modernization.md new file mode 100644 index 0000000..07615ed --- /dev/null +++ b/docs/migration/script-modernization.md @@ -0,0 +1,39 @@ +# Script Modernization Guide + +## Scope + +Standalone scripts still pending migration into `src/mini_projects/features`: + +- text/content: `compose.py`, `compose_template.py`, `lyrics.py` +- graph/image: `graph.py`, `graph_template.py`, `image.py`, `png.py`, `transform.py`, `transform_empty.py` +- data assets: `words.py` + +## Current Contracts + +- `compose.py`: word-transition graph and generated text composition. +- `graph.py`: weighted directed graph with probabilistic next-word sampling. +- `transform.py`: image transforms (brightness, contrast, blur, kernels, combine). +- `lyrics.py`: lyric retrieval with environment-based API key. + +## Stabilization Completed + +- Template implementations filled in `compose_template.py`, `graph_template.py`, `transform_empty.py`. +- `lyrics.py` moved to `GENIUS_API_KEY` usage and graceful skipping behavior. +- `compose.py` path fixed to `hp_socerer_stone.txt`. + +## Migration Requirements + +Move each script group into `src/mini_projects/features//` with: + +1. public entrypoint function +2. explicit input validation and clear errors +3. deterministic tests for core behavior +4. notebook demo import from modernized path + +## Curriculum Alignment + +- typed contracts and dataclasses: `advanced_python` +- deterministic algorithm tests: `dsa` +- retry/failure handling: `async_concurrency` +- boundary validation and adapters: `backend_api` +- analysis to dashboard to scale pattern: `data_stack`, `dashboard`, `big_data`, `capstone` diff --git a/docs/pipelines/world-bank/api-contracts.md b/docs/pipelines/world-bank/api-contracts.md new file mode 100644 index 0000000..f198cfb --- /dev/null +++ b/docs/pipelines/world-bank/api-contracts.md @@ -0,0 +1,63 @@ +# World Bank API and CLI Contracts + +## Upstream Endpoint Pattern + +`/country/{country_codes}/indicator/{indicator_id}?format=json&per_page={n}&page={p}` + +Example: + +`https://api.worldbank.org/v2/country/US;CA/indicator/NY.GDP.MKTP.KD.ZG?format=json&per_page=1000&page=1` + +## Internal Normalized Record Schema + +- `date` +- `country` +- `country_iso3` +- `indicator` +- `indicator_id` +- `category` +- `value` +- `fetched_at` + +## Backend Endpoints + +### `POST /world-bank/refresh` + +Request body: +- `force_refresh` (bool) +- `ttl_seconds` (int, 1..86400) +- `countries` (list[str]) +- `indicators` (list[str]) + +Response: +- `kpis` +- `dashboard` +- `processing_strategy` +- `record_count` +- `freshness` + +### `GET /world-bank/latest` + +Returns latest dashboard payload, refreshing if stale. + +### `GET /world-bank/status` + +Returns cache freshness metadata: +- `has_cache` +- `is_stale` +- `last_successful_fetch` +- `ttl_seconds` + +## CLI Commands + +Sync and build payload: + +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-sync --countries US CA GB DE JP --indicators NY.GDP.MKTP.KD.ZG FP.CPI.TOTL.ZG SL.UEM.TOTL.ZS +``` + +Freshness status: + +```bash +PYTHONPATH=src python -m mini_projects.cli world-bank-status +``` diff --git a/docs/pipelines/world-bank/operations.md b/docs/pipelines/world-bank/operations.md new file mode 100644 index 0000000..38d5299 --- /dev/null +++ b/docs/pipelines/world-bank/operations.md @@ -0,0 +1,42 @@ +# World Bank Pipeline Operations + +## Refresh Policy + +- Near-real-time mode (non-streaming). +- Request flow: + 1. Read cache by pipeline key. + 2. If fresh and not forced, serve cached payload. + 3. If stale or forced, fetch from World Bank, normalize, recompute payload. + 4. Persist payload with `last_successful_fetch` and `ttl_seconds`. + +## Caching Behavior + +- In-memory cache adapter currently used. +- Freshness is derived by `fetched_at + ttl_seconds`. +- Payload responses include explicit `freshness` metadata. + +## Verification + +Full test suite: + +```bash +PYTHONPATH=src pytest -q +``` + +Targeted suites: + +```bash +PYTHONPATH=src pytest -q tests/features/capstone tests/features/backend_api tests/features/data_stack +``` + +## Troubleshooting + +- Request failures: + - verify `api.worldbank.org` reachability + - check retry/backoff behavior in client +- Empty results: + - confirm valid indicator IDs and country codes + - some periods can have null values +- Unexpected stale results: + - inspect `/world-bank/status` + - verify TTL was not set too high diff --git a/docs/pipelines/world-bank/overview.md b/docs/pipelines/world-bank/overview.md new file mode 100644 index 0000000..fc463c5 --- /dev/null +++ b/docs/pipelines/world-bank/overview.md @@ -0,0 +1,33 @@ +# World Bank Pipeline Overview + +## Goal + +Provide a free, open, near-real-time pipeline for macro/development indicators using the World Bank Indicators API. + +## Source + +- API: World Bank Indicators API v2 +- Base URL: `https://api.worldbank.org/v2` +- Auth: none required + +## Architecture + +1. `world_bank_client.py` pulls paginated indicator data with retry logic. +2. Pipeline normalizes records and validates schema. +3. Data stack computes KPIs and aggregated category metrics. +4. Dashboard module builds payload tables for trend/category views. +5. Cache stores payload + freshness metadata. +6. API and CLI expose refresh and status operations. + +## Key Code Paths + +- `src/mini_projects/features/capstone/infrastructure/world_bank_client.py` +- `src/mini_projects/features/capstone/infrastructure/cache_store.py` +- `src/mini_projects/features/capstone/application/pipeline.py` +- `src/mini_projects/features/backend_api/presentation/app.py` +- `src/mini_projects/cli.py` + +## Related Docs + +- `api-contracts.md` +- `operations.md` diff --git a/docs/python_expansion_curriculum.md b/docs/python_expansion_curriculum.md new file mode 100644 index 0000000..639532b --- /dev/null +++ b/docs/python_expansion_curriculum.md @@ -0,0 +1,11 @@ +# Python Expansion Curriculum (Moved) + +The curriculum documentation has been reorganized into category-specific docs: + +- `docs/curriculum/overview.md` +- `docs/curriculum/modules/core_and_algorithms.md` +- `docs/curriculum/modules/systems_and_data.md` + +Start with the docs index: + +- `docs/README.md` diff --git a/docs/script_modernization_guide.md b/docs/script_modernization_guide.md new file mode 100644 index 0000000..e374a67 --- /dev/null +++ b/docs/script_modernization_guide.md @@ -0,0 +1,11 @@ +# Remaining Scripts Modernization Guide (Moved) + +This guide now lives at: + +- `docs/migration/script-modernization.md` + +Related curriculum docs: + +- `docs/curriculum/overview.md` +- `docs/curriculum/modules/core_and_algorithms.md` +- `docs/curriculum/modules/systems_and_data.md` diff --git a/docs/testing/verification.md b/docs/testing/verification.md new file mode 100644 index 0000000..373011d --- /dev/null +++ b/docs/testing/verification.md @@ -0,0 +1,27 @@ +# Verification Guide + +## Full Project Check + +```bash +PYTHONPATH=src pytest -q +``` + +## Focused Checks + +Curriculum modules: + +```bash +PYTHONPATH=src pytest -q tests/features/advanced_python tests/features/dsa tests/features/async_concurrency +``` + +World Bank pipeline and API: + +```bash +PYTHONPATH=src pytest -q tests/features/capstone tests/features/backend_api tests/features/data_stack +``` + +CLI behavior: + +```bash +PYTHONPATH=src pytest -q tests/test_cli.py +``` diff --git a/docs/world_bank_data_pipeline.md b/docs/world_bank_data_pipeline.md new file mode 100644 index 0000000..4e30b6b --- /dev/null +++ b/docs/world_bank_data_pipeline.md @@ -0,0 +1,11 @@ +# World Bank Data Pipeline (Moved) + +World Bank pipeline documentation has been split into focused docs: + +- `docs/pipelines/world-bank/overview.md` +- `docs/pipelines/world-bank/api-contracts.md` +- `docs/pipelines/world-bank/operations.md` + +Start with: + +- `docs/README.md` diff --git a/graph_template.py b/graph_template.py index 93c7794..165d140 100644 --- a/graph_template.py +++ b/graph_template.py @@ -2,41 +2,53 @@ class Vertex(object): def __init__(self, value): - pass + self.value = value + self.adjacent = {} + self.neighbors = [] + self.neighbors_weights = [] def add_edge_to(self, vertex, weight=0): - pass + self.adjacent[vertex] = weight def increment_edge(self, vertex): - pass + self.adjacent[vertex] = self.adjacent.get(vertex, 0) + 1 def get_adjacent_nodes(self): - pass + return self.adjacent.keys() # initializes probability map def get_probability_map(self): - pass + self.neighbors = [] + self.neighbors_weights = [] + for vertex, weight in self.adjacent.items(): + self.neighbors.append(vertex) + self.neighbors_weights.append(weight) def next_word(self): - pass + if not self.neighbors: + return self + return random.choices(self.neighbors, weights=self.neighbors_weights)[0] class Graph(object): def __init__(self): - pass + self.vertices = {} def get_vertex_values(self): - pass + return set(self.vertices.keys()) def add_vertex(self, value): - pass + self.vertices[value] = Vertex(value) def get_vertex(self, value): - pass + if value not in self.vertices: + self.add_vertex(value) + return self.vertices[value] def get_next_word(self, current_vertex): - pass + return self.vertices[current_vertex.value].next_word() def generate_probability_mappings(self): - pass + for vertex in self.vertices.values(): + vertex.get_probability_map() diff --git a/lyrics.py b/lyrics.py index b08e26a..8179d64 100644 --- a/lyrics.py +++ b/lyrics.py @@ -1,16 +1,26 @@ +import os +from pathlib import Path + import lyricsgenius -# generate an api key and paste it -# https://genius.com/api-clients -genius = lyricsgenius.Genius("api-key-here") +GENIUS_API_KEY = os.getenv("GENIUS_API_KEY") +if not GENIUS_API_KEY: + raise RuntimeError("Set GENIUS_API_KEY in your environment before running this script.") + +genius = lyricsgenius.Genius(GENIUS_API_KEY) def save_lyrics(songs, artist_name, album_name): + artist_folder = Path("songs") / "_".join(artist_name.split(" ")) + artist_folder.mkdir(parents=True, exist_ok=True) for i in range(len(songs )): song_title = songs[i] song = genius.search_song(song_title, artist_name) + if song is None: + continue lyrics = song.lyrics - with open('songs/{}/{}_{}_{}.txt'.format('_'.join(artist_name.split(' ')), i+1, album_name, '-'.join(''.join(song_title.split('\'')).split(' '))), 'w') as f: + output_name = "{}_{}_{}.txt".format(i + 1, album_name, "-".join("".join(song_title.split("'")).split(" "))) + with open(artist_folder / output_name, "w") as f: f.writelines(lyrics.split('\\n')) diff --git a/madlib.ipynb b/madlib.ipynb index 53d4242..224963d 100644 --- a/madlib.ipynb +++ b/madlib.ipynb @@ -1,102 +1,164 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "id": "26ba81f8", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Adjective: amazing\n", - "Verb: skydive\n", - "Verb: jump\n", - "Famous person:steve roger\n", - "Computer programming is so amazing! It makes me so excited all the time I love to skydive. Stay hydrated and jump like you are steve roger!\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Madlib\n", + "\n", + "Goal: build reusable text templates with validated inputs.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Quick verification checks\n", + "4. Demo rendering" + ], + "id": "119c9196" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Madlib notebook with template selection, validation, and replay-ready helpers.\"\"\"\n", + "\n", + "import random\n", + "\n", + "TEMPLATES = [\n", + " \"Computer programming is so {adjective}! I love to {verb1} and {verb2} like {famous_person}.\",\n", + " \"Today felt {adjective}. I decided to {verb1}, then {verb2}, just like {famous_person} would.\",\n", + " \"Learning Python is {adjective}; first I {verb1}, then I {verb2}, inspired by {famous_person}.\",\n", + "]\n", + "\n", + "\n", + "def validate_nonempty(text: str) -> str:\n", + " value = text.strip()\n", + " if not value:\n", + " raise ValueError(\"Input cannot be empty.\")\n", + " return value\n", + "\n", + "\n", + "def collect_words(input_fn=input) -> dict[str, str]:\n", + " schema = [\"adjective\", \"verb1\", \"verb2\", \"famous_person\"]\n", + " answers = {}\n", + " for key in schema:\n", + " answers[key] = validate_nonempty(input_fn(f\"{key.replace('_', ' ').title()}: \"))\n", + " return answers\n", + "\n", + "\n", + "def choose_template(rng: random.Random, preferred_index: int | None = None) -> str:\n", + " if preferred_index is not None:\n", + " return TEMPLATES[preferred_index]\n", + " return rng.choice(TEMPLATES)\n", + "\n", + "\n", + "def render_madlib(template: str, words: dict[str, str]) -> str:\n", + " return template.format(**words)\n", + "\n", + "\n", + "# Quick verification checks\n", + "sample_words = {\n", + " \"adjective\": \"exciting\",\n", + " \"verb1\": \"code\",\n", + " \"verb2\": \"debug\",\n", + " \"famous_person\": \"Ada Lovelace\",\n", + "}\n", + "assert \"exciting\" in render_madlib(TEMPLATES[0], sample_words)\n", + "assert choose_template(random.Random(1), preferred_index=1) == TEMPLATES[1]\n", + "try:\n", + " validate_nonempty(\" \")\n", + " raise AssertionError(\"Expected ValueError\")\n", + "except ValueError:\n", + " pass\n", + "\n", + "\n", + "rng = random.Random(7)\n", + "template = choose_template(rng)\n", + "print(render_madlib(template, sample_words))" + ], + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Adjective: amazing\n", + "Verb: skydive\n", + "Verb: jump\n", + "Famous person:steve roger\n", + "Computer programming is so amazing! It makes me so excited all the time I love to skydive. Stay hydrated and jump like you are steve roger!\n" + ] + } + ], + "id": "26ba81f8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "075913bf" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "dcc91ecf" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "f7b35039" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "a6d0eb6b" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "0d1d8ed1" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "1b51ed23" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "adj = input(\"Adjective: \")\n", - "verb1 = input(\"Verb: \")\n", - "verb2 = input(\"Verb: \")\n", - "famous_person = input (\"Famous person:\")\n", - "\n", - "madlib = f\"Computer programming is so {adj}! It makes me so excited all the time I love to {verb1}. Stay hydrated and {verb2} like you are {famous_person}!\"\n", - "\n", - "print(madlib)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "075913bf", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dcc91ecf", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f7b35039", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a6d0eb6b", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d1d8ed1", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b51ed23", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..506ee8e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +-r requirements.txt +pytest>=8.0 +pytest-asyncio>=0.23 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71410ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +numpy +pypng +lyricsgenius +pandas +matplotlib +fastapi +uvicorn +httpx diff --git a/src/mini_projects/__init__.py b/src/mini_projects/__init__.py new file mode 100644 index 0000000..fd61d1a --- /dev/null +++ b/src/mini_projects/__init__.py @@ -0,0 +1,2 @@ +"""Beginner mini-projects package.""" + diff --git a/src/mini_projects/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3808b35db63e2cdac1d735964d33ab5ece4f9aaf GIT binary patch literal 224 zcmey&%ge<81T&7l%v1-`k3k$5V1zP0a{w7r8G;##7}6OvnX2TSQqwc@@=}Wwax?QX zbqk8}vr>~wiWLeHld}`kQ}z5b8E>)2$EV~c$H%W^_zW`TR-k@pacWVqerjoYYF=t_ zX0Cn!RGWTIYGP5IZb4;9Mt+{Y5111G;S?7o>w~O_hgqQ?9}nb~#K-FuRNmsS$<0qG d%}KQ@VguR$a%?fkp&ytT85wUes1~sRIRGSHK`Q_N literal 0 HcmV?d00001 diff --git a/src/mini_projects/__pycache__/cli.cpython-313.pyc b/src/mini_projects/__pycache__/cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a74bb10942e1946d704fa443b2700e939146b1c GIT binary patch literal 4226 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 TfBxA)bgV9p9R-m%!uR argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="mini-projects", + description="Beginner Python mini-projects collection.", + ) + subparsers = parser.add_subparsers(dest="command") + + tictactoe_parser = subparsers.add_parser("tictactoe", help="Run TicTacToe.") + tictactoe_parser.add_argument( + "--difficulty", + choices=["easy", "medium", "hard"], + default="hard", + help="Computer difficulty level.", + ) + tictactoe_parser.add_argument( + "--seed", + type=int, + default=None, + help="Optional RNG seed for deterministic play.", + ) + subparsers.add_parser("curriculum-demo", help="Run curriculum expansion demo.") + world_bank_parser = subparsers.add_parser( + "world-bank-sync", + help="Fetch real World Bank indicators and build dashboard payload.", + ) + world_bank_parser.add_argument( + "--countries", + nargs="+", + default=["US", "CA", "GB", "DE", "JP"], + help="Country ISO2 codes. Example: US CA GB", + ) + world_bank_parser.add_argument( + "--indicators", + nargs="+", + default=["NY.GDP.MKTP.KD.ZG", "FP.CPI.TOTL.ZG", "SL.UEM.TOTL.ZS"], + help="World Bank indicator IDs.", + ) + world_bank_parser.add_argument( + "--ttl-seconds", + type=int, + default=3600, + help="Cache TTL seconds.", + ) + world_bank_parser.add_argument( + "--force-refresh", + action="store_true", + help="Force refresh even if cache is fresh.", + ) + + subparsers.add_parser( + "world-bank-status", + help="Show World Bank cache freshness status.", + ) + + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "tictactoe": + run_tictactoe_cli(difficulty=args.difficulty, random_seed=args.seed) + return 0 + if args.command == "curriculum-demo": + demo_records = [ + { + "date": "2026-01-01", + "country": "United States", + "country_iso3": "USA", + "indicator": "GDP growth (annual %)", + "indicator_id": "NY.GDP.MKTP.KD.ZG", + "category": "growth", + "value": 2.1, + "fetched_at": "2026-01-10T12:00:00+00:00", + }, + { + "date": "2026-01-01", + "country": "Canada", + "country_iso3": "CAN", + "indicator": "Inflation, consumer prices (annual %)", + "indicator_id": "FP.CPI.TOTL.ZG", + "category": "prices", + "value": 2.9, + "fetched_at": "2026-01-10T12:00:00+00:00", + }, + ] + result = run_capstone(demo_records) + print("Capstone KPIs:", result["kpis"]) + print("Processing strategy:", result["processing_strategy"]) + return 0 + if args.command == "world-bank-sync": + result = get_or_refresh_world_bank_dashboard( + force_refresh=args.force_refresh, + ttl_seconds=args.ttl_seconds, + countries=args.countries, + indicators=args.indicators, + ) + print("World Bank KPIs:", result["kpis"]) + print("Freshness:", result["freshness"]) + return 0 + if args.command == "world-bank-status": + status = get_world_bank_freshness() + print("World Bank status:", status) + return 0 + + parser.print_help() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/src/mini_projects/features/__init__.py b/src/mini_projects/features/__init__.py new file mode 100644 index 0000000..22c4353 --- /dev/null +++ b/src/mini_projects/features/__init__.py @@ -0,0 +1,2 @@ +"""Feature modules for mini projects.""" + diff --git a/src/mini_projects/features/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae54dfaae5bca5c8d7e64d87e3ef7556dbc31631 GIT binary patch literal 236 zcmey&%ge<81hbC5%+vzXk3k$5V1zP0a{w7r8G;##7}6OvnW~iBQWHx`i&7PG^HWN5 zQi~PR@{1I5GxIVP3X1ZxQj<%H_53s$Z?VV6r{pKc$FF4g3^M3elzwP&YEiL%YH50E zUTSe>u6_VixqeP+Vo{!ML1jrsexAM$m=gfu6c;7ygN%-c8LgiNvAkG6J{~Au5+AQu iPnBE(nFgMh~QnMAfA`dthJ?0O417TA3j1K;{$s2) wk;)Xxzjh2VjE}Ftcn4s06+l=vt+OtasB_%txsC4In7+wpN-1qgpl6nR0eve@Pyhe` literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/advanced_python/application/__init__.py b/src/mini_projects/features/advanced_python/application/__init__.py new file mode 100644 index 0000000..547d973 --- /dev/null +++ b/src/mini_projects/features/advanced_python/application/__init__.py @@ -0,0 +1 @@ +"""Application helpers for advanced Python module.""" diff --git a/src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1653266d621ccdc97f69ff2d0cd46b3c8b59bd6e GIT binary patch literal 277 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 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/advanced_python/application/patterns.py b/src/mini_projects/features/advanced_python/application/patterns.py new file mode 100644 index 0000000..19be376 --- /dev/null +++ b/src/mini_projects/features/advanced_python/application/patterns.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Callable, Generator +from contextlib import contextmanager +from dataclasses import dataclass +from time import perf_counter +from typing import Protocol, TypeVar + +T = TypeVar("T") + + +class SupportsLen(Protocol): + def __len__(self) -> int: + ... + + +@dataclass(slots=True) +class LearningTask: + name: str + minutes_estimate: int + topic: str + + +def retry(attempts: int) -> Callable[[Callable[..., T]], Callable[..., T]]: + if attempts < 1: + raise ValueError("attempts must be >= 1") + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + def wrapper(*args, **kwargs) -> T: + last_error: Exception | None = None + for _ in range(attempts): + try: + return func(*args, **kwargs) + except Exception as error: # pragma: no cover - exercised by tests + last_error = error + assert last_error is not None + raise last_error + + return wrapper + + return decorator + + +def sliding_window(items: list[T], size: int) -> Generator[list[T], None, None]: + if size <= 0: + raise ValueError("size must be > 0") + if size > len(items): + return + for start in range(0, len(items) - size + 1): + yield items[start : start + size] + + +@contextmanager +def timed_operation() -> Generator[Callable[[], float], None, None]: + start = perf_counter() + result = {"elapsed": 0.0} + + def elapsed_seconds() -> float: + return result["elapsed"] + + try: + yield elapsed_seconds + finally: + result["elapsed"] = perf_counter() - start + + +def describe_size(value: SupportsLen) -> str: + return f"size={len(value)}" diff --git a/src/mini_projects/features/async_concurrency/__init__.py b/src/mini_projects/features/async_concurrency/__init__.py new file mode 100644 index 0000000..1dc5135 --- /dev/null +++ b/src/mini_projects/features/async_concurrency/__init__.py @@ -0,0 +1 @@ +"""Async and concurrency learning module.""" diff --git a/src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecd33e47b4594f33084b3e360d2f0dd37c364f78 GIT binary patch literal 258 zcmYjML2AP=5R{VAgh2kFQy>sW9|(coO9+IV7b9<66?-J-lu)iZ2N8Uj{58y+b1dCj*C)d@BZ^@{XGG!c@h8fAU zs=$=VQm#suH3e=DYuU{JGG_ojMIX(&LZ3ad*VyDp=xwQw?B3DTUjLsbRa?I~(y!4g zIS$0L(WNB|)u_E-S~rYdr!fgUq|g9CSp#gVwwv#UD0_Ym&tn}QtNc!-l%3e{Qy_mH CVoxyu literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/async_concurrency/application/__init__.py b/src/mini_projects/features/async_concurrency/application/__init__.py new file mode 100644 index 0000000..c7c22b0 --- /dev/null +++ b/src/mini_projects/features/async_concurrency/application/__init__.py @@ -0,0 +1 @@ +"""Application services for async and concurrency.""" diff --git a/src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d477e724245de0282c108cee51c5e577e8c5101e GIT binary patch literal 279 zcmYjMK}rKb5S-DVLC7EGnuF<6Jc;04Bp{xfVYZWu?Dh;jvy$~6KEk&MK47n&yb1dO zXQLtZrK_N;t7@3dE>xNKk1u~!_UA|rqyNA#E#Or>C{Y_dKY4-M7;Eor_JK_zdG?Ny zSqCvTb?l7g%DBL;6(M%r;(o6CbHpk*#5XmJU;7%Yu4#N_I%dsW7vV&0gG%~2)h zi@7=FnH56*N6f4cvAvpMdLd3~661I_ zbIAV&xh+OzADDs)ec;8_;$l(&=m4F>MV%-R4Fq9Ya#jRzgEoNrTQ)Pgo)5Wf zcS^UCb2_yZs++k2P3pG0s^4zNXr`6Wxh|5hWlouFlPZ7gi{ZGbQ!}g=W_3e1)7kJO z&0WxwmKo0I8a4Lh=dHP%5&j16OgbHtCd1jZkxt}myeVC?3b=fjTbD@YjAVgQ-AK-d zwR}F4PV!(0=hJyTlQ#6h{Jh=7%}%_Tw&oJd+;u#54;L{!G=ww>APS8R|)rm11(7#HC>&CSn3B z7o@DXM!Fv6>!ihD!XZP6Lo4j7PL06E#Oeb~A&CdHD@vKiE_JW*cp{U|rmc0MovbD` zSIGo!F);Wg5@Uk1_tOG_aZLHO39Q0tf_s8>DYq`c5d`tKuX-xu2#V|FxurYM+75g@ zK^OqegJ-vzw!67e05fP~3L&Eh6Iq4XHEH_+iGBbfkv1YH^5lp(OaVrTKlW?ozWYkUzx{H;;K8+M1Dy2x_%@gaosl}zDr(m{Zt$gf9}J9 zUz1_iZ(K^ofA^4C*(QAaOPs3j@f;2t}y+h^Rp^wI&n#}gA?VX6Od{J98Eo!?EQVP_dDo)}A z19OU%23;4cxz2XYY8K)qLawKVoTlU7=I6qhvaL{Nh{=kX`YrXy5y$7v&uxP;qdDto zB%alftm~Z@7u;Jpl#MPl9w;jZmYX^jn}YY7g1D1~rs3lF7m^^qPyuW{R{)`@`)2n) zg3;3WLNHoxo>=rwl%ErhWQ!vC7gSSZzZ$cDrJ8mnT)pG?ad&o1|Fnbe`6>#BM&eE2$YtJ!8AD z*y+X$i$1fEu|gh-`m2}`sH#brI!4!w=>)6H!NHbhat4-dCY&~AZHbMtD$cZ+CflG> zud#Kat~I3@Xp>VutC}fG-Vna#6>M18J#K(wBuW;1JZ*mb%|*X@-><&c@qXt*@THIa zL#6TJv9d3^ygB%bfp-V)7Vd36zSwsBd#8%8eA3i$OXPa0~ z_CW~C5l`|!7SAn>x09r9n(RQgSaaw!cD9KeO;oQ?iMjdnG(CfWrKEEXTHBsvzECCQ zZ2F=;b7`3QB4N$v^?FG|U#Yp&0ktMIpw^`ZR4x@F9CFy*xGrwHv1a7a1d}CN&5@nS zxm?cF*Itk!6rmk#F>P2B zvm6`{Rd_5v^JLPd#Ra%ldW13TU`>QIoc}0NxGIl*Mz7%<#I7x;GC8fPoAXNaAY)Tm zv$|q~-H=G63hZK;NKm$ws&|vQ4E}?`|NR)$&L^qFrdSB@DIhZy}?P(c{=9_?kb%rXmtT{t3OGLF1 str: + if retries < 1: + raise ValueError("retries must be >= 1") + last_error: Exception | None = None + for _ in range(retries): + try: + return await fetcher(resource) + except Exception as error: + last_error = error + await asyncio.sleep(0) + assert last_error is not None + raise last_error + + +async def run_bounded_tasks( + resources: list[str], + fetcher: Callable[[str], Awaitable[str]], + concurrency_limit: int = 3, +) -> list[str]: + if concurrency_limit < 1: + raise ValueError("concurrency_limit must be >= 1") + semaphore = asyncio.Semaphore(concurrency_limit) + + async def guarded(resource: str) -> str: + async with semaphore: + return await fetcher(resource) + + return await asyncio.gather(*(guarded(resource) for resource in resources)) + + +async def fanout_with_timeout( + resources: list[str], + fetcher: Callable[[str], Awaitable[str]], + timeout_seconds: float, +) -> list[str]: + tasks = [asyncio.create_task(fetcher(resource)) for resource in resources] + done, pending = await asyncio.wait(tasks, timeout=timeout_seconds) + for task in pending: + task.cancel() + results: list[str] = [] + for task in done: + results.append(task.result()) + return results + + +def choose_concurrency_model(task_type: str) -> str: + mapping = { + "io_bound": "asyncio", + "cpu_bound": "multiprocessing", + "mixed": "threading_or_asyncio_hybrid", + } + if task_type not in mapping: + raise ValueError("task_type must be one of: io_bound, cpu_bound, mixed") + return mapping[task_type] diff --git a/src/mini_projects/features/backend_api/__init__.py b/src/mini_projects/features/backend_api/__init__.py new file mode 100644 index 0000000..0df1808 --- /dev/null +++ b/src/mini_projects/features/backend_api/__init__.py @@ -0,0 +1 @@ +"""Backend API learning module.""" diff --git a/src/mini_projects/features/backend_api/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ec75c342ecfb2cab6056b52016983f849532b97 GIT binary patch literal 242 zcmey&%ge<81P{->%v1)_k3k$5V1zP0a{w7r8G;##7}6OvnW|)*5|gu2^HLNX13VRS zQWJ~vGV{_Ea`RJ4b5iyEG#PKP$H%ASC&$OHWcUm+;#Qh|XmM&$v3_c4dTL&3ab~W5 zKv8~HYH~@jK3KbML1jrsexAM$$SmCe2&cFxSwA;3FEhRXrZz1#v81#pwOBt1Vt;&M tL8g9uJWx$Ze7s&kxb`Uhr4M#ftVN<}O{4ggy)N8|ti literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/backend_api/domain/__init__.py b/src/mini_projects/features/backend_api/domain/__init__.py new file mode 100644 index 0000000..7141539 --- /dev/null +++ b/src/mini_projects/features/backend_api/domain/__init__.py @@ -0,0 +1 @@ +"""Domain models for backend API module.""" diff --git a/src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccd35469ab4428fc4aa2267892464e5c9cc31f7f GIT binary patch literal 258 zcmey&%ge<81W(Sr%+vwWk3k$5V1zP0a{w7r8G;##7}6OvnW|J>@^ce2^AvLPQ&Mw^ z71HvH6p|8?vs3d@6dVIQLBgdusd|2zjJMe1<5TjJCpEDsPq(15BqKjh-v`VIfN+Y7lJ#>l^D^TLU~1D+6H7{q zQj7JW*2X6mWa_7Y9i<;14^&zbAFo$Xd5gma;ugCiHlXbw?-YZ)@`0I=k?|ITb`cAZ F0|0o$P2&In literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc b/src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..476add176229f917f3782ffce5955f2438d6c6f0 GIT binary patch literal 667 zcmYjOzi$&U6t>TINt%!V5-mTbQVF3O;1)U{AqKXlRaEIPB6W*gY6qKSAvzL z1AhYlOeMC;QY9qBkge4XtUTW}6_)(o_xXL#e$THS4*Lk!`T1IW2mjlX&5Bxp>vsUo zP>ND~h{jmsU5|sqU`#aGMN_heQj&(R@goE%dV$ z6-pYQTRF2VE0{6wfkmcJ)?_z{jUz3=^(z2pXo@sWkq%O<$ulTq8m8nqMA8sYbc;F- z>gs6Gi?>`jQI@YeAag`W>$*?@ry_U6SnVP!Y{6YPQ~Bx#n#3K~qg1kzQ|kJZmMX6b zkMB_Wxnjjl_YqgkFmK#d-2+F8oBX^Q~FkG92 zR|_t=5oL0$)hC`=lN6k3`C_@UAC*jw8gkqcqqC$GQqbi_JLk++nww8`HFrG@0}i93`VEpi~jJ-f!^L|CUN9jpI823sM!y} z4Q@xpjSe@`rk^%*$E$j8+7ElH9|Og(O$_FWV2m%(-XHYzx`Tqjw+BD*&fkFG(G>zy M6BInW_O!X`A0v*jaR2}S literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/backend_api/domain/models.py b/src/mini_projects/features/backend_api/domain/models.py new file mode 100644 index 0000000..65dfe80 --- /dev/null +++ b/src/mini_projects/features/backend_api/domain/models.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(slots=True) +class Note: + note_id: int + title: str + body: str diff --git a/src/mini_projects/features/backend_api/infrastructure/__init__.py b/src/mini_projects/features/backend_api/infrastructure/__init__.py new file mode 100644 index 0000000..0575f84 --- /dev/null +++ b/src/mini_projects/features/backend_api/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Infrastructure adapters for backend API module.""" diff --git a/src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc24681666bcfee138192af8bd74348e5665019a GIT binary patch literal 276 zcmY*UF-`+95VQe7gp@zHO+mJTDng=+gwRQddS!fj!6au}Yr6>l;1PU=2V6^m=#cIM z*d!@3&CW&&)TfXwd$@}s@}JGL-U8394-)BZxBDkSg#Hlo?@{8-H z*rpfn;Qn7UI}zDHZ=J<(*zHKy2od&gQ3nh5C>|gdOG2E7?J!yFGTridNl^>NTJHB$ LN;!yYJ}cr6X%AKI literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc b/src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a173f85f085eec29c6f76252fdf2403b9a7ec043 GIT binary patch literal 2625 zcmb7GO>Ep$5Pr7z$8oaV{3uGAw27NmRklsD;U|Lrh_Ve^n!in0?^cNdvK+^oxY@PS z_q+;;klGVU1ybQqCIhGrziWR96ta713sJGBWRNR=cchlWet<-mzH}B2N z8^8Hx=6O33kqC_SyARF&AR!NMQ5gZB;kI7~<_1xS!i|ylI zP9flgqkE(I=eG@wX|fIJN<_2?3~kkX(8Jua+-s5l>vMcjSpA#E(#%yewJ z@=%0h_42oQuHeQ5oO0Wrc@Ag1O(f}Rt@3UU$#M#(@F)2!$?wXmY(l%{yG)j(%Te6j z0w%w^2lD!AiO&y~w`4L;VnXUX1glA3F!V*0X$3=d7O0`k#X@eFUCx`1@roPPZObxr z$Cb(q#Ml+m_Oe?~?PWEm>3Dy4pK4gFNDY<2X$39Dy8&k8b8ci#bF^8F8S0YeEHJ#A zEW69`EHfyJ8^w9UGMHJ2Pg465cwzCpp;7C^(u%WSTk$b(G3hTD)#C-zGS#JuZqCpg z@X6w{n!adQbE>vv#!V|nHRjNwj&WW9_T?(`TZtRAQ_YJ-17;Z?n3kB1O zakLTk8AeQ|%|K}s^$zX@xF$jOp~-O|R|$ZKaBhg*C9!*p4@DYx1hG!qY9@6NU(i(& zyY7iHZ3TV38ZRtM58>~Dc`<~inqTEB&CC*>ksHrNc_s`_y)lDH>y50C_L^>G?a!5L znrem?BOJlPU+o)CDamvO!JY6kpw|PfPexc`^2NkhHmUTU?UT>OPTv;Xa5)>?pr_^r zJhhOA32g&Ev>geyA6GCf$F0j_723<@TrA)rL=U1u7b=AGB4svZsEwKC!RFTM(0m}} z(39J-2fiv<2UxKSKmfa@*3Y{??cQkWEj9ISHk{gwG_Q?s2Zd&7Ewm-p0Jtv7_oFRe z4Sf^ZkOxZgz;6d$`l+=P9r;xp@qw)_T@0uP>@8qk4A@Gr)6W6V;<8)|xyUma8`mRg-L#VKOyx*21|=S0@UYnBo<1oo>7ux$)QZ8cKhTi zQ04RKiScr^Po&?1ias}8QIr$P&}n)Ad^~}PcES?C!?N=rcEz0G=nGmt@8KL1JY3xX zpv+tWDHg{ro>Jaqj)x|5_wRx8iKqummf zRNEp#o%G)$0xZJ-{?L_(2B2f$mEcEVRXkNN3h;iNf@?Oxo-07kx*=-7+hO5vgk{8d zihY9i0f~kD=p4fh%w+0uo*Z+zth?&ndRs?rub9UB?N0MT9`ZmX&?{L9f*&2gwX3tf<@>TTlXuiyHxBT*#y zL&C5wKP3P1La=K`l7feJ+C#xn?r~!{DET(139mhTSFVW9B{)V1DK-&zpysvhue`SX zRj+OToL$gNs~>J< None: + self.connection = sqlite3.connect(database_path, check_same_thread=False) + self.connection.row_factory = sqlite3.Row + self._ensure_schema() + + def _ensure_schema(self) -> None: + self.connection.execute( + """ + CREATE TABLE IF NOT EXISTS notes ( + note_id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + body TEXT NOT NULL + ); + """ + ) + self.connection.commit() + + def create_note(self, title: str, body: str) -> Note: + cursor = self.connection.execute( + "INSERT INTO notes(title, body) VALUES(?, ?);", + (title, body), + ) + self.connection.commit() + return Note(note_id=int(cursor.lastrowid), title=title, body=body) + + def list_notes(self) -> list[Note]: + rows = self.connection.execute( + "SELECT note_id, title, body FROM notes ORDER BY note_id ASC;" + ).fetchall() + return [ + Note(note_id=row["note_id"], title=row["title"], body=row["body"]) + for row in rows + ] diff --git a/src/mini_projects/features/backend_api/presentation/__init__.py b/src/mini_projects/features/backend_api/presentation/__init__.py new file mode 100644 index 0000000..1da82df --- /dev/null +++ b/src/mini_projects/features/backend_api/presentation/__init__.py @@ -0,0 +1 @@ +"""Presentation entrypoints for backend API module.""" diff --git a/src/mini_projects/features/backend_api/presentation/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/presentation/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6f8b73fd9c3c700b6a3b6053093b1f49ef88c25 GIT binary patch literal 275 zcmYk1O-chn5QV!{Gz4;o-ezISm2QHNWk6&=To>(3C3ceOZmN0~<2^itm+=6zb>&K7 zE?|#IAa7IeLH&HyPbOoP_4E1d?sEMa!cqEQ`cXFT>PfL$>*?9sO~EAOlT9volMS~q zIG@C<0-G0GZpl|>Q7lazs%A&C$EohF5UWrkeyDy}+X*}iu>dt|@+7W?g2RT&B(NjP z{yMhlHF$XV55+zRmry(JFdlnXWK)xI;BXdgbP!MY05K0F#93^+(R@#I&zG5D_)mu4 Oe$Tbmox0_V9R2_}=2fTw literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc b/src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e77be4e26847ce1db89ed2ee591687dba009b92e GIT binary patch literal 4994 zcmb6dTWlQF_0H_f?91L=+wlVj>@{(mE-||loR%dIf#W#Iu49bH#tse))#{r2_%f%fb2UGooqLjH<_T9e(5u>TwA+$Jhfg)~VCRET0AFCVtcGHpcX64Rz=FDE ze(GBd#Dc8jJT-L3a1Z|ahG9E*BS60^Ab0}+Tq3Y;T3XJqR-HBI>u=Qh9^0Wz zc1$N{nEwF9FljlT-TatL$NbEvX_mfjXc`M>+Il{_k;8tQroFzQ=c<&jrmdLNc55fCqxmhfziwn4JCQSVYCT@qbk_1#Vu}xD+<{Fq3AmxD6>4)=KqMQWuf(#RdCjo0nqDvy z1wa^2^cX$Usu0A*$m1Gqu%bo6Wwec~y1j1@23e5xXi-HCK>cYNfOBV)yh zo5%hvjnRP`TZ@9LrY-D`0r{YURq>jndbsK&=*jgSSF`G+@~RKS-sUROE`U87ceyA% zrsXg%Sh=B@S$YJwMm(1&7u|GBp?!#X2thxBqX-^G@CX7FuzTo1>vzRu$E&`jQ^%1@D*D zK%EiY#zu&R7=<&AMJVYq*Avx12zRuat;$$4JPIcdJKpx=r}peHZplD+v=<$P(h!w@==S;r>X^P7zc?`yzmlGt ziKi#xFJ0UUP0qy6&s>hrJwG?a$Ldsk{=${&SUv1I6wR8R0uY6t?FCQ-x0Qwh{M;;H zS`d46IIvwc%iy7A83RVwHhH*s9EU$fkVJ5py^bIO0V>?S008W@f3SGv)>uUvywm&e z_S{GMCzIvg)5Y|yu8MT}&e3T3hYRJSi^a<~H!ITOorj0M5v7L$-*}Wqlw#_Z|68KS zifiMz?sNvjWv!u+bKSoS*jIeF$vinL{GdwBffJ0uwGx4_MtMzbcRY{6!=e%!BP`$8{uW2n0tQD-y zZG2U^v4ZMT{Yj77#rYu>xA033;1AYMe?)~u-L#|pfYZ0vIl9$Q3y1Omj&Ku4q=myf zk6zd_z17E4K-5hE(Q!A2T&wmMf%McmD*Pm`I<64-2|F(=s5)78l0g#O3%O=%5_u z$t?5^w%NMNHYhdOs=_migy^7=h7@ZrG((sZXK{}M?f>5eIO-1FSnPYxB_~BoSgO0W zs(_x+=y;nKn)M8p?VV=CyahJ2)y*Gz(F}i=MVF1}at@xcAj*rt$&{?&=w=Rv-7suC zE@EEyK_^4@F$)wg&4N`WqanC6hmeQc2V`36re}H{krM!H6mzs@CsQzBgNNBk)3TUr z>{S40D8bI3pMB@-PH>S`MH5U}bym{Ty^kzLTz# zb|v^L{&9TYC#p~W!|m!i9PM^f8UQ__FC1uqMWGyMfUSq~2I+81P=%!AK074J6K89> zjL8%Y;ZTx5uK>x$@Db%6$~y0_V%W%GinxE?XG)x1zi-+l;I>gNsG}2= zaIEBu@$*8^R;U3Lk+l`V@)%!|qfvZLg1cV@hOJKiz?=quo~X>FJWwPv3HRX(F$vO#0~JqzClX$7GY=$HdJb7I znNW-rbOwQnU;zPF3qBm9)%L7w+ku9N=&L}+_nQ4R00IL$Tt-7lDpa z-;?FQQ^oUN$-Wy4Z!O%sT9JGAML~}4dx+AvhrK<8D1J^{{4o2gGv$$|iwh+=`7O|< z1f2E^PMZ?kd5do?-dy|e%}=|EizWHOx1uDE;+#og54(G9xE9PQg^pF^;eAn&14ur; zhrPW>b*EDMP(^+e&vA+Ga13|2RNVoN*`Pm$+?@Ov?B5nHrns8vi*Q{~&@1rS>UNb` z`zs^*W^_7l8C+#tAF*yM>{m9hPKFX3BQy(Ubw|(SblWy;9?hBJY!*ywmEt>sc~{`S z6sV**zncksh0UyPK|xP3i}CuOooT7X2e1Vss;e`^Ot5zzv3tJz$B9 zUa%dwMjY%UhqVU$l@l+R1q1(qa6 FastAPI: + repo = repository or NoteRepository() + app = FastAPI(title="Learn Python Backend API") + + @app.get("/health") + def health() -> dict[str, str]: + return {"status": "ok"} + + @app.get("/notes", response_model=list[NoteResponse]) + def list_notes() -> list[NoteResponse]: + return [NoteResponse(**asdict(note)) for note in repo.list_notes()] + + @app.post("/notes", response_model=NoteResponse, status_code=201) + def create_note(request: CreateNoteRequest) -> NoteResponse: + if request.title.strip() == "": + raise HTTPException(status_code=400, detail="title cannot be blank") + note = repo.create_note(title=request.title.strip(), body=request.body.strip()) + return NoteResponse(**asdict(note)) + + @app.post("/world-bank/refresh") + def refresh_world_bank(request: RefreshRequest) -> dict[str, Any]: + return get_or_refresh_world_bank_dashboard( + force_refresh=request.force_refresh, + ttl_seconds=request.ttl_seconds, + countries=request.countries, + indicators=request.indicators, + ) + + @app.get("/world-bank/latest") + def latest_world_bank_dashboard() -> dict[str, Any]: + return get_or_refresh_world_bank_dashboard(force_refresh=False) + + @app.get("/world-bank/status") + def world_bank_status() -> dict[str, Any]: + return get_world_bank_freshness() + + return app diff --git a/src/mini_projects/features/big_data/__init__.py b/src/mini_projects/features/big_data/__init__.py new file mode 100644 index 0000000..4bd26cc --- /dev/null +++ b/src/mini_projects/features/big_data/__init__.py @@ -0,0 +1 @@ +"""Big-data analysis learning module.""" diff --git a/src/mini_projects/features/big_data/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/big_data/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17d1a6fca6686e36e5f23b654689cb0b0a04e846 GIT binary patch literal 245 zcmXv}F^===YDDzB;6z(Lk(go~)_z`|h`fF3AGu#I( z#W9&?XJB?`c9_raMZ)6i$2_F@oy86Ej|{U!j$$KN)Uq5M%4hEzqg6D{7J>NFXbQ#7 zaj`Y^vQkkMt*g3*R;>>R Fi3>-SP(AZivwOZ2xcJ(y1Uy7mJ>+6u_3k8~2Oo6x_g;PV)vK!a4jYXs zkp1(+)8soF;8){JjeKL);YAuBKo`2!8eFr`@(s4v>}w7>guSk_47W>I>Xy5u0=WaX zRe5R_w=7*_LS%}mBomU{%14SL>!mzC);Bb8>2u6pzY}->q1Cn8Z2(pS*t876Sl7OC zj5*B&N=O!uq+rrXxjSrh748V#-XrvQD*W&JIH(B3Tnmvg>B z`Znp$Oy^$xuN*@k);j<-@F8^GK8~^3|53EhLCe`da}m3z<6F7dj%2dS{b4SZ-{byO z|6;7k%xdTxdt;L+!>XXGk>f3qzB-`btQh3+Kj>BQXw!iAbW+l)J^5%8|>U zmkgplQ!Jn?( int: + if chunk_size <= 0: + raise ValueError("chunk_size must be > 0") + total = 0 + chunk: list[int] = [] + for value in values: + chunk.append(value) + if len(chunk) == chunk_size: + total += sum(chunk) + chunk.clear() + if chunk: + total += sum(chunk) + return total + + +def estimate_scale_strategy(row_count: int) -> str: + if row_count < 1_000_000: + return "pandas_local" + if row_count < 50_000_000: + return "dask_out_of_core" + return "spark_distributed" diff --git a/src/mini_projects/features/capstone/__init__.py b/src/mini_projects/features/capstone/__init__.py new file mode 100644 index 0000000..d9d2326 --- /dev/null +++ b/src/mini_projects/features/capstone/__init__.py @@ -0,0 +1 @@ +"""Integrated capstone learning module.""" diff --git a/src/mini_projects/features/capstone/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/capstone/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2652320e597126e8c3b977b69ed3a42706e984a5 GIT binary patch literal 247 zcmXv}K?=e!5KL551Rvn72QMl$9}w??h#;PqlCCvkld?$<`VSxBYrOaB$(zs*n5f;$ z&cN)a?lsHTynH1I;jnNqs(n@Dh zZqo9M^UZ`;J%H3mfIF*$RVBsVB3XfDh8nHP#g@zorL*FD9v2sPG+L~KSG@fZOH$-Y sD<~T60TWjeTG50H055O=^WswV7rzEe>iW#^gAM)PZO(bcrquD|1LXlq5C8xG literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/capstone/application/__init__.py b/src/mini_projects/features/capstone/application/__init__.py new file mode 100644 index 0000000..e7e478f --- /dev/null +++ b/src/mini_projects/features/capstone/application/__init__.py @@ -0,0 +1 @@ +"""Application services for capstone module.""" diff --git a/src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c85096cca9c8f5dcd752a067dd8231d0fb3cbd4 GIT binary patch literal 264 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 LKXOBhzSVyK#Us?b literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/capstone/application/pipeline.py b/src/mini_projects/features/capstone/application/pipeline.py new file mode 100644 index 0000000..83491ad --- /dev/null +++ b/src/mini_projects/features/capstone/application/pipeline.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from mini_projects.features.big_data.application.chunked_processing import ( + estimate_scale_strategy, +) +from mini_projects.features.capstone.infrastructure.cache_store import ( + CacheEntry, + InMemoryCacheStore, +) +from mini_projects.features.capstone.infrastructure.world_bank_client import ( + WorldBankClient, + WorldBankObservation, +) +from mini_projects.features.dashboard.application.dashboard import ( + build_indicator_dashboard_payload, +) +from mini_projects.features.data_stack.application.analysis import ( + load_indicator_frame, +) + +CACHE_KEY = "world_bank_dashboard_payload" +DEFAULT_COUNTRIES = ["US", "CA", "GB", "DE", "JP"] +DEFAULT_INDICATORS = [ + "NY.GDP.MKTP.KD.ZG", # GDP growth + "FP.CPI.TOTL.ZG", # Inflation CPI + "SL.UEM.TOTL.ZS", # Unemployment + "SP.POP.TOTL", # Population + "BN.CAB.XOKA.GD.ZS", # Current account + "SI.POV.DDAY", # Poverty headcount +] +INDICATOR_CATEGORY_MAP = { + "NY.GDP.MKTP.KD.ZG": "growth", + "FP.CPI.TOTL.ZG": "prices", + "SL.UEM.TOTL.ZS": "labor", + "SP.POP.TOTL": "demographics", + "BN.CAB.XOKA.GD.ZS": "external", + "SI.POV.DDAY": "poverty", +} + +_DEFAULT_CACHE = InMemoryCacheStore() + + +def run_capstone(records: list[dict[str, object]]) -> dict[str, object]: + frame = load_indicator_frame(records) + dashboard = build_indicator_dashboard_payload(frame) + strategy = estimate_scale_strategy(len(frame)) + return { + "kpis": dashboard["kpis"], + "dashboard": dashboard, + "processing_strategy": strategy, + } + + +def get_or_refresh_world_bank_dashboard( + *, + force_refresh: bool = False, + ttl_seconds: int = 3600, + countries: list[str] | None = None, + indicators: list[str] | None = None, + cache_store: InMemoryCacheStore | None = None, + client: WorldBankClient | None = None, +) -> dict[str, Any]: + if ttl_seconds < 1: + raise ValueError("ttl_seconds must be >= 1") + store = cache_store or _DEFAULT_CACHE + cached_entry = store.get(CACHE_KEY) + now = datetime.now(tz=UTC) + if ( + cached_entry is not None + and not force_refresh + and not cached_entry.is_stale(now=now) + ): + payload = dict(cached_entry.payload) + payload["freshness"] = { + "served_from_cache": True, + "ttl_seconds": cached_entry.ttl_seconds, + "last_successful_fetch": cached_entry.fetched_at.isoformat(), + "is_stale": False, + } + return payload + + wb_client = client or WorldBankClient() + normalized_records = _fetch_and_normalize_world_bank_records( + client=wb_client, + countries=countries or DEFAULT_COUNTRIES, + indicators=indicators or DEFAULT_INDICATORS, + ) + frame = load_indicator_frame(normalized_records) + dashboard = build_indicator_dashboard_payload(frame) + payload = { + "kpis": dashboard["kpis"], + "dashboard": dashboard, + "processing_strategy": estimate_scale_strategy(len(frame)), + "record_count": len(normalized_records), + "freshness": { + "served_from_cache": False, + "ttl_seconds": ttl_seconds, + "last_successful_fetch": now.isoformat(), + "is_stale": False, + }, + } + store.set( + CACHE_KEY, + CacheEntry(payload=payload, fetched_at=now, ttl_seconds=ttl_seconds), + ) + return payload + + +def get_world_bank_freshness( + cache_store: InMemoryCacheStore | None = None, +) -> dict[str, Any]: + store = cache_store or _DEFAULT_CACHE + entry = store.get(CACHE_KEY) + if entry is None: + return { + "has_cache": False, + "is_stale": True, + "last_successful_fetch": None, + "ttl_seconds": None, + } + now = datetime.now(tz=UTC) + return { + "has_cache": True, + "is_stale": entry.is_stale(now=now), + "last_successful_fetch": entry.fetched_at.isoformat(), + "ttl_seconds": entry.ttl_seconds, + } + + +def _fetch_and_normalize_world_bank_records( + *, + client: WorldBankClient, + countries: list[str], + indicators: list[str], +) -> list[dict[str, object]]: + normalized: list[dict[str, object]] = [] + for indicator_id in indicators: + observations: list[WorldBankObservation] = client.fetch_indicator_observations( + indicator_id=indicator_id, + countries=countries, + ) + category = INDICATOR_CATEGORY_MAP.get(indicator_id, "other") + for row in observations: + normalized.append( + { + "date": row.date, + "country": row.country, + "country_iso3": row.country_iso3, + "indicator": row.indicator, + "indicator_id": row.indicator_id, + "category": category, + "value": row.value, + "fetched_at": row.fetched_at.isoformat(), + } + ) + return normalized diff --git a/src/mini_projects/features/capstone/infrastructure/__init__.py b/src/mini_projects/features/capstone/infrastructure/__init__.py new file mode 100644 index 0000000..6423e6a --- /dev/null +++ b/src/mini_projects/features/capstone/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Infrastructure adapters for capstone real-data ingestion.""" diff --git a/src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdceb0a86793e31f70ced27583b85c8d6ab88bd4 GIT binary patch literal 283 zcmY+9!Ab)$5QY<5v=sUdx%SZJEJ*8BkWvuOA!KLQ$Zis5W)Br#!-w#3yzbSLH(_7E zMBPLG%ghJ=4F5l_R!fof`tjv&^ZYC@N&YY6A}?>^L7-^l`r_sKF4Sm=v3J}fC|lXY zfTZe(%GpF511JRBZYs-G`OpBdk70XQ%i+qHDmr7{#aR9is2?dGYv>yYK)%yQjL+bh zbPE>4CY|^x27Uhv$3Gx+y7R%CGVB@F2)fc9r8wK3Vx#%$ARSlo26>RZ_1WsXpK PQt9|TlTr?1kMj&aB-B|A literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc b/src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..deb51a34a95525c2789aab7a8518c829acf92c32 GIT binary patch literal 2054 zcmZuy&2JM&6rb4-du?M(h)E&>>JkF&woTlEQURi$sD~zPAYLq03e{>^kIkl=-E?M5 z<)}zKwQ#85l0&OX^oqnE(LVu`1JOvRl~514L5-Aa-`ll;q>QZJysz2$&71ee{c^cT zU|jEQhfg#@o}e-uW+u@7br3d)OI*4@<|(H(%G!c9uX8<>^#x<@`Z48r8h2LP!?F23|E;dAU0VfeF)S)jk}j&Hbs<35e)WACp}YW|1%Ia z$Q8n=OStAzuAhMOT`^q!P4Kt|NMsHT-$QWs(8Tl9fwh5;@;OJnV8yN=)7GnNZO zLxa@Q${6a|>}qG}42b9M6EaO4eGxLN$#uZ|fVKno4*^P#lkYiJ5Z{lGM>qfg*JTJ6 zwRja2Rp6|}IEw=&@HTAQR-IZyFfMAW(`GRf;cD#?PgYnUMJ-}Jk7w4p@>&wtzD$)% znId>lTMgsTTN}oELvd${7n8XPi`8MTL&gKjft(1>ZVcMF*LJIpAp1tF|OOyQiIZ?eazMFmU8p1 zh7H3F*K+k#GTnk}q*Bf`dA^-k2%toe}4= zXdQbEXIO-nDG-2eW%TxqTQ?q6>|VwGV|24};(>J{BmXLu7vybNwL#!jfO+p|P<-Y8QwZ-MU_$w6 zglU95t#J}<2v~$-1mGv~T%+cx=Q(Pg+yOYA5yzNCr-Uur>fj3lZHC!Zr5AryOmG7% z=>wU)jrc68ob0ZJahqfFWxUf(cN0<8`Rpb5h@XQvEFAGMz*C)4`j|}pLyrALW_o1i nF&TT7BUk|>Inxq+n)DPtgTNkIV@ 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 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/capstone/infrastructure/cache_store.py b/src/mini_projects/features/capstone/infrastructure/cache_store.py new file mode 100644 index 0000000..36945d3 --- /dev/null +++ b/src/mini_projects/features/capstone/infrastructure/cache_store.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from typing import Any + + +@dataclass(slots=True) +class CacheEntry: + payload: dict[str, Any] + fetched_at: datetime + ttl_seconds: int + + def is_stale(self, now: datetime | None = None) -> bool: + reference = now or datetime.now(tz=UTC) + return reference >= self.fetched_at + timedelta(seconds=self.ttl_seconds) + + +class InMemoryCacheStore: + def __init__(self) -> None: + self._entries: dict[str, CacheEntry] = {} + + def get(self, key: str) -> CacheEntry | None: + return self._entries.get(key) + + def set(self, key: str, value: CacheEntry) -> None: + self._entries[key] = value diff --git a/src/mini_projects/features/capstone/infrastructure/world_bank_client.py b/src/mini_projects/features/capstone/infrastructure/world_bank_client.py new file mode 100644 index 0000000..01a69cb --- /dev/null +++ b/src/mini_projects/features/capstone/infrastructure/world_bank_client.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import UTC, datetime +import time +from typing import Any + +import httpx + + +class WorldBankClientError(RuntimeError): + """Raised when the World Bank API request fails.""" + + +@dataclass(frozen=True, slots=True) +class WorldBankObservation: + country: str + country_iso3: str + indicator: str + indicator_id: str + date: str + value: float + fetched_at: datetime + + +class WorldBankClient: + def __init__( + self, + base_url: str = "https://api.worldbank.org/v2", + retries: int = 3, + timeout_seconds: float = 20.0, + ) -> None: + if retries < 1: + raise ValueError("retries must be >= 1") + self.base_url = base_url.rstrip("/") + self.retries = retries + self.timeout_seconds = timeout_seconds + + def fetch_indicator_observations( + self, + indicator_id: str, + countries: list[str], + per_page: int = 1000, + ) -> list[WorldBankObservation]: + if not indicator_id.strip(): + raise ValueError("indicator_id is required") + if not countries: + raise ValueError("countries must not be empty") + if per_page < 1: + raise ValueError("per_page must be >= 1") + + country_segment = ";".join(country.upper() for country in countries) + endpoint = f"/country/{country_segment}/indicator/{indicator_id.upper()}" + + observations: list[WorldBankObservation] = [] + page = 1 + + while True: + payload = self._request_json( + endpoint=endpoint, + query_params={"format": "json", "per_page": per_page, "page": page}, + ) + metadata, rows = self._validate_response_payload(payload) + observations.extend(self._parse_rows(rows)) + if page >= int(metadata["pages"]): + break + page += 1 + + return observations + + def _request_json(self, endpoint: str, query_params: dict[str, Any]) -> list[Any]: + url = f"{self.base_url}{endpoint}" + last_error: Exception | None = None + for attempt in range(1, self.retries + 1): + try: + response = httpx.get(url, params=query_params, timeout=self.timeout_seconds) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, list): + raise WorldBankClientError("World Bank API payload must be a list") + return payload + except Exception as error: # pragma: no cover - exercised by tests via retries + last_error = error + if attempt < self.retries: + time.sleep(0.1 * attempt) + + raise WorldBankClientError(f"failed request to {url}") from last_error + + def _validate_response_payload( + self, + payload: list[Any], + ) -> tuple[dict[str, Any], list[dict[str, Any]]]: + if len(payload) != 2: + raise WorldBankClientError("unexpected payload format; expected [metadata, rows]") + metadata = payload[0] + rows = payload[1] + if not isinstance(metadata, dict): + raise WorldBankClientError("metadata section must be an object") + if "pages" not in metadata: + raise WorldBankClientError("metadata missing 'pages'") + if not isinstance(rows, list): + raise WorldBankClientError("rows section must be a list") + return metadata, rows + + def _parse_rows(self, rows: list[dict[str, Any]]) -> list[WorldBankObservation]: + parsed: list[WorldBankObservation] = [] + fetched_at = datetime.now(tz=UTC) + for row in rows: + value = row.get("value") + if value is None: + continue + country_info = row.get("country", {}) + indicator_info = row.get("indicator", {}) + parsed.append( + WorldBankObservation( + country=str(country_info.get("value", "unknown")), + country_iso3=str(country_info.get("id", "UNK")), + indicator=str(indicator_info.get("value", "unknown")), + indicator_id=str(indicator_info.get("id", "unknown")), + date=str(row.get("date", "")), + value=float(value), + fetched_at=fetched_at, + ) + ) + return parsed diff --git a/src/mini_projects/features/dashboard/__init__.py b/src/mini_projects/features/dashboard/__init__.py new file mode 100644 index 0000000..7f9d2e7 --- /dev/null +++ b/src/mini_projects/features/dashboard/__init__.py @@ -0,0 +1 @@ +"""Dashboard building learning module.""" diff --git a/src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fc7484fa0e9319f08cd5ddd10c8a16a36b2772a GIT binary patch literal 247 zcmXv}K?=e!5KL55g!%@-Qw7Zj#FIx6K|C+DS!<+C%BDTkfA|Pr-)dEob#H6)N|qkv)4($ literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dashboard/application/__init__.py b/src/mini_projects/features/dashboard/application/__init__.py new file mode 100644 index 0000000..4993f06 --- /dev/null +++ b/src/mini_projects/features/dashboard/application/__init__.py @@ -0,0 +1 @@ +"""Application services for dashboard module.""" diff --git a/src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60ef6ea228b08e8edd70e7c9ebee39de5c778b23 GIT binary patch literal 266 zcmYLEK}rKb5S)pU1R>xP_L_icy^G*oBp{xf$!sSX*_|1>XNC9=AK`1f@6}Uof&D;c zO`>(_DyZ(R>L!ybm2v&Ob9Z_E^x_PCBHcKXPxYc$t@U*9F?)=$a@Hgld?Cc|&XN?X zz(r|fvkZpIq7G$K(foO;+Yw?JEaI2y`h#tR*SsV^&6+%kt6{<6jcgLA$gsbO+q4ND zo_@vRD1Xt9F FH~=wsQD*=E literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc b/src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db628c0efe5c561735150e11079af77d278f9797 GIT binary patch literal 1378 zcmZ`&&2QX96rZuZwzD5C61|PpxNzW&H=CqQc?ZvXzc=&zK7Q}8Rx2Zr z-SM+1R1tbAjkzc@1s|?}@({Vm#cQaI8E#5lSyPw_r@E%KORUt^nGU$_Y8TNxjTxYo z3eD^(w((4cvazU;BuR6UM`^;>ZADl?+Rp|#^`j(=0+Of9zn?{1)KBd0&<6+YrEC~r z8RkfII3Z(v_%A3A(Z;dQLrX_`f;ZsCOirPShdQM>esX$HpM*Oy@=MY&Ujo2%6<1wW zT&;LhIqP3Kf;zRcJ{P;C53#E+<3#nar*zK0;*Q~(%h*%@-{&s0)XT^&uL~`N7Og^q zO~Pd_=>t*vj>H2h)Q}7TH-a=C^b;-&MuU`v956j)5lwPi6w>ho$#ukaH)dgeBLrVSYDFoZE$QXJ&8~IQ=Mz z{A_-AlahSED0f1_?{-tdLWg8oEGY)mJ3(97P%L%_Q5>FP;PD$jBf~f)A-f3a$qDnH z;j@FDR2F}Ecfa!f&aEf)4<0Y=*Kh2s|6$gCto=~irH@vB>QAgIW9!Pqx;nP5?$PgS zBXi}qugB&W6SF-w+xzD2iRq3__n?01k+=KhR7DG4;VDuU%Al5Bn^w_<`a$EpNy8a8 zoW0?w;fxxe9axJK%N|?yUSnk0BkPmD46Uk9kyg?VAuII4a!A@2%kd^CuU-xy4&J`; zISKA>L3^&tMTF+SWSUzHdPP}tGi6QsAmVwH^qK*QV;D@!#zi;aupl&-o7Q9v_=-1X zeLUN}x46`{7PS=hQoa&4C=S?=xAd&@+<2kA;$j;^P;n(3(@B1uH^)oKV_G}B@5@Ws;P=G{vEyZ6uthBrefuJwTkt>-&XKP H#oPY?R1seL literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dashboard/application/dashboard.py b/src/mini_projects/features/dashboard/application/dashboard.py new file mode 100644 index 0000000..63fff1d --- /dev/null +++ b/src/mini_projects/features/dashboard/application/dashboard.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import pandas as pd + +from mini_projects.features.data_stack.application.analysis import ( + compute_indicator_kpis, + indicator_by_category, +) + + +def build_indicator_dashboard_payload(frame: pd.DataFrame) -> dict[str, object]: + kpis = compute_indicator_kpis(frame) + category_table = indicator_by_category(frame) + trend_table = ( + frame.groupby(frame["date"].dt.date, as_index=False)["value"].mean().rename( + columns={"date": "day"} + ) + ) + return { + "kpis": kpis, + "category_rows": category_table.to_dict(orient="records"), + "trend_rows": trend_table.to_dict(orient="records"), + } + + +def build_dashboard_payload(frame: pd.DataFrame) -> dict[str, object]: + """Backward-compatible wrapper for existing callers.""" + return build_indicator_dashboard_payload(frame) diff --git a/src/mini_projects/features/data_stack/__init__.py b/src/mini_projects/features/data_stack/__init__.py new file mode 100644 index 0000000..a1be363 --- /dev/null +++ b/src/mini_projects/features/data_stack/__init__.py @@ -0,0 +1 @@ +"""Data analysis learning module.""" diff --git a/src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16ae86885bec4dd0aa9cb2f27d378c01dc80fab5 GIT binary patch literal 243 zcmXwzJqp4=5QTSDRD@hXQfOhJdjYYs6bXp+uq0z#O*Ua?Q^b3C2v1|5*2+%E1>E&# znt2c2y!qN77%?}a`%_Kbet7dA{uAxci7VSN!%{x$T*hl@Whk}GYEl$tD2-M+4f7(Y zGF)tDycqx_MGSCbtuJX%>yMVgVLq-3*sshL!2Nub!Vg`>V@2Z`j22oOuISoJbu2 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/data_stack/application/__init__.py b/src/mini_projects/features/data_stack/application/__init__.py new file mode 100644 index 0000000..ea2ab30 --- /dev/null +++ b/src/mini_projects/features/data_stack/application/__init__.py @@ -0,0 +1 @@ +"""Application services for data analysis.""" diff --git a/src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad48727d091c1144f3e81655484d15c534cbe867 GIT binary patch literal 264 zcmYLEK}rKb5S-DVLChcaCOKsL10r~r5D?GJFx!cvyE9|=Y+&;bAK`1f@70qxVL#xE z8mvoKL3MZ4v{+oIjLY{=SLgXr#1Zr!6Lq?sazXh;301aP`@Kj;yT!Hcp{qwS~BccaY&EB!`+YA z{0d4CX%-AZB|)tm81wue;FnE#2^0PEbloDfD%!wI0=q HXIXp$Zn994 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc b/src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d65b69169ef887b7b2ca6b1d31ce7cb3738b841f GIT binary patch literal 5058 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 b%Zv6fkV8(|@gigg((I-k|A;tfBZmG9gKHFM literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/data_stack/application/analysis.py b/src/mini_projects/features/data_stack/application/analysis.py new file mode 100644 index 0000000..ec8c910 --- /dev/null +++ b/src/mini_projects/features/data_stack/application/analysis.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import pandas as pd + + +@dataclass(frozen=True, slots=True) +class NormalizedIndicatorRecord: + date: str + country: str + country_iso3: str + indicator: str + indicator_id: str + category: str + value: float + fetched_at: str + + +def load_sales_frame(records: list[dict[str, object]]) -> pd.DataFrame: + frame = pd.DataFrame(records) + required_columns = {"date", "category", "revenue", "orders"} + missing = required_columns - set(frame.columns) + if missing: + raise ValueError(f"missing columns: {sorted(missing)}") + frame["date"] = pd.to_datetime(frame["date"]) + frame["revenue"] = pd.to_numeric(frame["revenue"]) + frame["orders"] = pd.to_numeric(frame["orders"]) + return frame + + +def load_indicator_frame(records: list[dict[str, object]]) -> pd.DataFrame: + frame = pd.DataFrame(records) + required_columns = { + "date", + "country", + "country_iso3", + "indicator", + "indicator_id", + "category", + "value", + "fetched_at", + } + missing = required_columns - set(frame.columns) + if missing: + raise ValueError(f"missing columns: {sorted(missing)}") + frame["date"] = pd.to_datetime(frame["date"], errors="coerce") + frame["value"] = pd.to_numeric(frame["value"], errors="coerce") + frame["fetched_at"] = pd.to_datetime(frame["fetched_at"], errors="coerce") + if frame["date"].isna().any(): + raise ValueError("date contains invalid values") + if frame["value"].isna().any(): + raise ValueError("value contains invalid values") + frame = frame.sort_values(["date", "country_iso3", "indicator_id"]).reset_index(drop=True) + return frame + + +def compute_kpis(frame: pd.DataFrame) -> dict[str, float]: + total_revenue = float(frame["revenue"].sum()) + total_orders = int(frame["orders"].sum()) + average_order_value = total_revenue / total_orders if total_orders else 0.0 + return { + "total_revenue": total_revenue, + "total_orders": float(total_orders), + "average_order_value": round(average_order_value, 2), + } + + +def revenue_by_category(frame: pd.DataFrame) -> pd.DataFrame: + return ( + frame.groupby("category", as_index=False)["revenue"] + .sum() + .sort_values("revenue", ascending=False) + ) + + +def compute_indicator_kpis(frame: pd.DataFrame) -> dict[str, float | str]: + latest_date = frame["date"].max().date().isoformat() + latest_fetch = frame["fetched_at"].max().isoformat() + return { + "observation_count": float(len(frame)), + "country_count": float(frame["country_iso3"].nunique()), + "indicator_count": float(frame["indicator_id"].nunique()), + "latest_observation_date": latest_date, + "mean_value": round(float(frame["value"].mean()), 4), + "latest_fetch_at": latest_fetch, + } + + +def indicator_by_category(frame: pd.DataFrame) -> pd.DataFrame: + return ( + frame.groupby("category", as_index=False)["value"] + .mean() + .rename(columns={"value": "average_value"}) + .sort_values("average_value", ascending=False) + ) diff --git a/src/mini_projects/features/dsa/__init__.py b/src/mini_projects/features/dsa/__init__.py new file mode 100644 index 0000000..47ccf72 --- /dev/null +++ b/src/mini_projects/features/dsa/__init__.py @@ -0,0 +1 @@ +"""Data structures and algorithms learning module.""" diff --git a/src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fcef85d841fa3459cd76c59602cbbfb66a58c85 GIT binary patch literal 253 zcmXv}F>V4u4BRDAK#};vwiMthRER2N6d^^lSgSkdSai30viAz|k31ru$P@B_YpLiW z-3PJ@VH(fK_KZDDr;jS(`{(SIsh+a9LI0CslE|leQ>-dI8-2jDjn+uyu819z80$-8 z>ngB|`$kMnmc8?pX+qi6w0NEAevDX#0`W@?*|p!nR)__ttH_hM1~wcHR78Qx`n)~H zeem#-z2=*%5H6r`-eG&2eJ6W40ZOren0gU$(H{HB>KeZ0hZL7Tk^IH)oz}WnPkf)m EA93DH(f|Me literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dsa/application/__init__.py b/src/mini_projects/features/dsa/application/__init__.py new file mode 100644 index 0000000..72aabb3 --- /dev/null +++ b/src/mini_projects/features/dsa/application/__init__.py @@ -0,0 +1 @@ +"""Algorithms for DSA expansion module.""" diff --git a/src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fc003dce37c096544bfd953d3f1ee0f8adf39d7 GIT binary patch literal 254 zcmXw!L23d)5JkIVFcR<%vv6Z1)&+tfWEn&d*F`f^jx96YO;ry>?~y~~7M>snm~B?s z1?B?j(O8@M4?gwlcjNJ+N^}1Dv9q+ld$^7M7u_g{54BOON+0JwJ zx>;uQ9*h&~T~_;|sc62M>h=M#@HyhA>iSdd0k%RcKut-G#MZE8f1o@HRAksahhyA( z2X7ZxUqLvB+B%Ej=G~Er4U<42222Q*%}q?vfS7y{aUPEC=;ivzf`=)(uOfYo-+|V; JRZo1M;16TOOw|AY literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc b/src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20fe735743689a34291bbd9ac2edc52062ac9c5e GIT binary patch literal 5822 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 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/dsa/application/algorithms.py b/src/mini_projects/features/dsa/application/algorithms.py new file mode 100644 index 0000000..ac34e59 --- /dev/null +++ b/src/mini_projects/features/dsa/application/algorithms.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import heapq + + +class TrieNode: + def __init__(self) -> None: + self.children: dict[str, TrieNode] = {} + self.is_terminal = False + + +class Trie: + def __init__(self) -> None: + self.root = TrieNode() + + def insert(self, word: str) -> None: + node = self.root + for char in word: + node = node.children.setdefault(char, TrieNode()) + node.is_terminal = True + + def search(self, word: str) -> bool: + node = self.root + for char in word: + if char not in node.children: + return False + node = node.children[char] + return node.is_terminal + + def starts_with(self, prefix: str) -> bool: + node = self.root + for char in prefix: + if char not in node.children: + return False + node = node.children[char] + return True + + +class UnionFind: + def __init__(self, size: int) -> None: + if size <= 0: + raise ValueError("size must be > 0") + self.parent = list(range(size)) + self.rank = [0] * size + + def find(self, node: int) -> int: + if self.parent[node] != node: + self.parent[node] = self.find(self.parent[node]) + return self.parent[node] + + def union(self, left: int, right: int) -> None: + root_left = self.find(left) + root_right = self.find(right) + if root_left == root_right: + return + if self.rank[root_left] < self.rank[root_right]: + self.parent[root_left] = root_right + elif self.rank[root_left] > self.rank[root_right]: + self.parent[root_right] = root_left + else: + self.parent[root_right] = root_left + self.rank[root_left] += 1 + + def connected(self, left: int, right: int) -> bool: + return self.find(left) == self.find(right) + + +def dijkstra( + graph: dict[str, list[tuple[str, int]]], + start: str, +) -> dict[str, int]: + distances = {node: float("inf") for node in graph} + distances[start] = 0 + queue: list[tuple[int, str]] = [(0, start)] + + while queue: + current_distance, node = heapq.heappop(queue) + if current_distance > distances[node]: + continue + for neighbor, weight in graph[node]: + candidate = current_distance + weight + if candidate < distances.get(neighbor, float("inf")): + distances[neighbor] = candidate + heapq.heappush(queue, (candidate, neighbor)) + return {key: int(value) for key, value in distances.items() if value != float("inf")} + + +def longest_increasing_subsequence(values: list[int]) -> int: + if not values: + return 0 + tails: list[int] = [] + for value in values: + index = _lower_bound(tails, value) + if index == len(tails): + tails.append(value) + else: + tails[index] = value + return len(tails) + + +def _lower_bound(items: list[int], target: int) -> int: + left, right = 0, len(items) + while left < right: + middle = (left + right) // 2 + if items[middle] < target: + left = middle + 1 + else: + right = middle + return left diff --git a/src/mini_projects/features/tictactoe/__init__.py b/src/mini_projects/features/tictactoe/__init__.py new file mode 100644 index 0000000..16168f9 --- /dev/null +++ b/src/mini_projects/features/tictactoe/__init__.py @@ -0,0 +1,2 @@ +"""TicTacToe feature package.""" + diff --git a/src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7441951a8d67b295abb7d6d83d7ba1dbdfc75a9d GIT binary patch literal 238 zcmXwzy$!-J6ohR+5Fup&s)#NH@dAhm5Q;>opDgDQ3`rc>P7(HC0xAY!iL_L7NLc`f zpKIQI(&^oGzCR^S*ZWh>-F@`%7yX&ebLK*}M3an$gYztrBHfK&;98|nH7ZOV2AwP9FiSfY^yi?uqUu!iNxb-Ab-dr&1?e<1#X(SjASlu-S% rzbeD56qXCCkO8=a1&FGq^_GQ7>l{~l>`MFTrl0*9Qc7DA=#hgrDq%*I literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/tictactoe/application/__init__.py b/src/mini_projects/features/tictactoe/application/__init__.py new file mode 100644 index 0000000..aaa327d --- /dev/null +++ b/src/mini_projects/features/tictactoe/application/__init__.py @@ -0,0 +1,2 @@ +"""Application services for TicTacToe.""" + diff --git a/src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c513a9d1a68205574f6230bc60cdf774e0f2488 GIT binary patch literal 259 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 a2PYN@RQpaC7IFBcFUoPDzY(n2G5-y=4HMr0 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/tictactoe/application/ai.py b/src/mini_projects/features/tictactoe/application/ai.py new file mode 100644 index 0000000..e6dc8c5 --- /dev/null +++ b/src/mini_projects/features/tictactoe/application/ai.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import math +import random +from dataclasses import dataclass +from typing import Optional + +from mini_projects.features.tictactoe.domain.board import Board + + +@dataclass +class MinimaxResult: + position: Optional[int] + score: int + + +class TicTacToeAI: + def __init__(self, letter: str, difficulty: str = "hard", random_seed: Optional[int] = None): + if letter not in {"X", "O"}: + raise ValueError("AI letter must be X or O.") + if difficulty not in {"easy", "medium", "hard"}: + raise ValueError("Difficulty must be one of: easy, medium, hard.") + self.letter = letter + self.difficulty = difficulty + self._random = random.Random(random_seed) + + def choose_move(self, board: Board) -> int: + legal_moves = board.available_moves() + if not legal_moves: + raise ValueError("No legal moves available.") + + if self.difficulty == "easy": + return self._random.choice(legal_moves) + + if self.difficulty == "medium": + # Medium intentionally mixes predictable strength and mistakes. + if self._random.random() < 0.45: + return self._random.choice(legal_moves) + return self._best_move(board, max_depth=2) + + return self._best_move(board, max_depth=None) + + def _best_move(self, board: Board, max_depth: Optional[int]) -> int: + if len(board.available_moves()) == 9: + return self._random.choice(board.available_moves()) + + result = self._minimax( + state=board, + player=self.letter, + alpha=-math.inf, + beta=math.inf, + depth=0, + max_depth=max_depth, + ) + if result.position is None: + raise RuntimeError("AI failed to choose a move.") + return result.position + + def _minimax( + self, + state: Board, + player: str, + alpha: float, + beta: float, + depth: int, + max_depth: Optional[int], + ) -> MinimaxResult: + max_player = self.letter + other_player = "O" if player == "X" else "X" + + if state.current_winner == other_player: + score = state.num_empty_squares() + 1 + if other_player != max_player: + score *= -1 + return MinimaxResult(position=None, score=score) + + if not state.is_empty(): + return MinimaxResult(position=None, score=0) + + if max_depth is not None and depth >= max_depth: + return MinimaxResult(position=None, score=0) + + if player == max_player: + best = MinimaxResult(position=None, score=-math.inf) + for possible_move in state.available_moves(): + state.make_move(possible_move, player) + sim_score = self._minimax(state, other_player, alpha, beta, depth + 1, max_depth) + state.cells[possible_move] = " " + state.current_winner = None + sim_score.position = possible_move + + if sim_score.score > best.score: + best = sim_score + alpha = max(alpha, best.score) + if beta <= alpha: + break + return best + + best = MinimaxResult(position=None, score=math.inf) + for possible_move in state.available_moves(): + state.make_move(possible_move, player) + sim_score = self._minimax(state, other_player, alpha, beta, depth + 1, max_depth) + state.cells[possible_move] = " " + state.current_winner = None + sim_score.position = possible_move + + if sim_score.score < best.score: + best = sim_score + beta = min(beta, best.score) + if beta <= alpha: + break + return best + diff --git a/src/mini_projects/features/tictactoe/application/game.py b/src/mini_projects/features/tictactoe/application/game.py new file mode 100644 index 0000000..aed22ed --- /dev/null +++ b/src/mini_projects/features/tictactoe/application/game.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from mini_projects.features.tictactoe.domain.board import Board + + +class MoveProvider(Protocol): + letter: str + + def get_move(self, board: Board) -> int: + """Return a move index from 0 to 8.""" + + +@dataclass +class GameResult: + winner: str | None + board: Board + + +def play_game(x_player: MoveProvider, o_player: MoveProvider) -> GameResult: + board = Board() + letter = "X" + + while board.is_empty(): + current_player = o_player if letter == "O" else x_player + square = current_player.get_move(board) + if not board.make_move(square, letter): + raise ValueError(f"Move {square} is not valid.") + if board.current_winner: + return GameResult(winner=letter, board=board) + letter = "O" if letter == "X" else "X" + + return GameResult(winner=None, board=board) + diff --git a/src/mini_projects/features/tictactoe/domain/__init__.py b/src/mini_projects/features/tictactoe/domain/__init__.py new file mode 100644 index 0000000..7de75fc --- /dev/null +++ b/src/mini_projects/features/tictactoe/domain/__init__.py @@ -0,0 +1,2 @@ +"""Domain models for TicTacToe.""" + diff --git a/src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61d33cd77c3b2a3f3a4612b848b2b590b9b08543 GIT binary patch literal 247 zcmey&%ge<81WS*;%v1r=k3k$5V1zP0a{w7r8G;##7}6OvnW|)5@^ce2^AvLPQ&Mw^ z71HvH6hbnSLlToi@>BKvG#PKP$H%ASC&$OHWcUm+;#RhPXmM&$v3_c4dTL&3ab~W5 zKv8~HYH~@jeoks)QJ!u=Wl2VUp1u#569C~97bWZGX69wa7r@k}r6!h?7Nr*Jmt-cF zBqo>Sr|PGGU7{Z!4^&tZAFo$Xd5gma=&sV7RJ$TJpxq!p6ocIVftit!@fL$p5etw5 E0NGqg)&Kwi literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc b/src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..130a3216b2feb8d3728d1b3197785e01aef749e3 GIT binary patch literal 4356 zcmcgvT}&L;6~6Pcv%l<`#Wn<+dcc^lZeVeU9kVzP*A7Z;z(3=4N$qqp><(nueE0m_bJr>=0tDJGe!rTryoCG>nR4^E1h>`?#9g8gg*#8qag>vh=Fjuz z1S*^psmOTYyyu)mrE^~DmC1FXhBOdGtR;%)IPb1=+J+4U(0GVy$Z@CTacm_1VXsAFhiBt!IE9uNV6k)%C_Q$u2HjPY~21ZYsPyS zbJ1linu?ER%#1o#mL1YHJ4baZZf8=UTsy1B)7eoiW5x#&YU|jz<2z+&ql0O!%Lz01 ztz{lDS?D5@WHGgvd~)gWdvp)5V7W+IAJTG$tq$RubbP{(cDiQS^nJ&Z(hbAvpQtDs z%^eP>a+K<(t=`O-rcP5FtW<6i5KKQ5h`Z$4#$1wIE_qN{6raaS^Ht<3ca^t~od4Z8!w7 zRtt2KC^}COYL~WXY{R2ukbwB)KvO_%Z=EpENN5c_VjSDi0!M3svrvVlE%-8)s*jG@<7EdZ z^XQEh?sAf>M4~h4dn0#73K2Q)lbIiE0fb6nCOiqmU9vlz%P6(2DO}l{WuQC*3VxK$ znKqR{elyxiFpvvZwLZ#e;M-2MtMVNaQGB%;I>cXfdt!QGIdZ5FIW#X6BaL}qqq~dE zm_!g}yT}6pyWqf)V3jmDDA0W3$AoA>vdKXep^RSV!0p6N(Can{&nAgo-8so&wpFCW zO?VgHANCH&ySaUm!;}S@EhT4QHrhb6oNAj(V;W!{kd2Mq4f|sLeu{pxie5EwNMYKO zM{|}f59;uIa zyhCZ0y2g{-D5v{jk(j_-&XHVuIg-Iv+ZspyQSIAq2sshgoErleh}JN4V8&qO%;}F4 zv-{`ke_H!AdN5ztRg8A$L)|bjTU!hsW@E>T(G&U5iIrgFc49g)GgJuH%~utJjVo0( zpVl3|9heTxoGAEealUi9bLPf!u&EGiT4`ur3LOGR+36OT(3x8_fKtB2yN=NUv?=WF z=t)80Fu%i-$EbHXk(lDO4TB{4H#kLr6U3Jkt4R5TEIZ4F8POp`bi@`gsOh=3Vf>eY z19?!g_)-=Gjb}gDEf)-2bx|N5Tm*6=EaH9@-bJ!;RT0nw(<&}c$bhHhq!)*`G%paeXe)QdkXV%v+4Sv6JFL&kt1D>J*8GL#IKa0ANWfwEW^MZCjcNC{K zA!>ivQ$p0UP7^S>iW!MzFvuuKuOd#0k@98YjKlZVUgC@<9{cBWvpqlQe(ZN~0=iQO z>@Db4AvnHNbg$!=yRk!+^#B%3pN^mzhU1}{IjqM5EFDwK1I&jUFU{UmGiKTmfN;F2 ztid=g<48c~MolQqGuki~;KyE)ud~`vmOp@62pe-jijM7sHalMfj9BZ?eGd6MS~Gk4 z&R3_rE8*zvk?E1y>iJ`H2OfAA>lcEH9~E9bnGbhN2~UIJrEuHAnPRwYIoMVRwyjiE ze+q9NyK{8P`}ECIPcHrPR-n0*(@1nFnt0?d zMia}CL?Mz`sXp*u39m%1^+@?UTFfggGHcTj%l?!gY#Ex1j!@%Zh$piI~ZaYEO zAdF!lSgMNlzcQ1Yy>#cjDe;Gam9I2RiGTDR*jd|Qre@f_3B(td=eyM>$qYnJ2w6=_LefTre59~W6-^Vw-fo#yb+#;gV#QE-@L?em}<3E3k?q4ra4PnGs+nCb@BQ z6=vPw5#Z9pglQ7S%CcfVKr1n9H?cjAPgPBAR996epu%M#or61r>V#Ak3NfQJ6H(Qn z47F?{W9nvBRVnJ;)dWm!x-Y{Nob-g&VONhGW^{_liNzqri#xpxowH9>Excu?RL!<& zW-w>#7QBwty{iT+@m-ap-m%!RPDA$t^4IEvg_f>Db@${uQyoRId!?~?vVW$pC^o+k zIZq$=LgGC2*d2Kx1wDIT)J8q~UNrNb!=L#$&t=YC9oOuS`Ek=jIlPR*xA6pUf*bdJ z-J&d&<7OSn9v{n?!xa65A{ZCiR=99$bv4*U2SEg5!omaOnZR+}pGm{tN%LPw{WFR1 W!O!@1?lSj`V8157z(3dk1NTpkqIj49 literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/tictactoe/domain/board.py b/src/mini_projects/features/tictactoe/domain/board.py new file mode 100644 index 0000000..4ec1de2 --- /dev/null +++ b/src/mini_projects/features/tictactoe/domain/board.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + + +@dataclass +class Board: + cells: List[str] = field(default_factory=lambda: [" "] * 9) + current_winner: Optional[str] = None + + def available_moves(self) -> List[int]: + return [idx for idx, value in enumerate(self.cells) if value == " "] + + def is_empty(self) -> bool: + return " " in self.cells + + def num_empty_squares(self) -> int: + return self.cells.count(" ") + + def make_move(self, square: int, letter: str) -> bool: + if square < 0 or square > 8: + raise ValueError("Square must be between 0 and 8.") + if letter not in {"X", "O"}: + raise ValueError("Letter must be X or O.") + if self.cells[square] != " ": + return False + + self.cells[square] = letter + if self.winner(square, letter): + self.current_winner = letter + return True + + def winner(self, square: int, letter: str) -> bool: + row_ind = square // 3 + row = self.cells[row_ind * 3 : (row_ind + 1) * 3] + if all(value == letter for value in row): + return True + + col_ind = square % 3 + column = [self.cells[col_ind + i * 3] for i in range(3)] + if all(value == letter for value in column): + return True + + if square % 2 == 0: + diagonal1 = [self.cells[i] for i in [0, 4, 8]] + if all(value == letter for value in diagonal1): + return True + diagonal2 = [self.cells[i] for i in [2, 4, 6]] + if all(value == letter for value in diagonal2): + return True + return False + + def reset(self) -> None: + self.cells = [" "] * 9 + self.current_winner = None + + def clone(self) -> Board: + return Board(cells=self.cells.copy(), current_winner=self.current_winner) + diff --git a/src/mini_projects/features/tictactoe/presentation/__init__.py b/src/mini_projects/features/tictactoe/presentation/__init__.py new file mode 100644 index 0000000..ad5caab --- /dev/null +++ b/src/mini_projects/features/tictactoe/presentation/__init__.py @@ -0,0 +1,2 @@ +"""Presentation layer for TicTacToe.""" + diff --git a/src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53993fbafeb8255094bd4c6fb8c4fa6ba9d4fda4 GIT binary patch literal 258 zcmYk1K~BR!3`J)sDpjT2A*@gq)E7XU0HH{PwKbZ!Wl%B`JkBEAgF|o(7F;6RuDV0Y z1v&vL^>6&2^kw@`S+-V5U*88;q`9BrB>iuuRWcvyNwM1L;^O_fWQyd)hzmY%%s`xP z1Lu{qm9bTz&10d*E5tfj#7{NNd&W0-j>HkD-;t;2S|~ZZl9dPz8TPl`ATPng!!MM7 z5ZOZOyueN0y-u^m!$-LMo(GjtQWft+F~*}P)Xj&Q0# z$CW&tH=tz)l!+vK z&@zm-zr&5E&>Ioa$u^hgFmQD_523rhzQ=GC^q8J|jCoreV?_Xr_;!-Bd7|jUs?BHB zeBQMA0?~BK<}MkUo@8zQ0+ox6HJc^IHIp(CMbXZuGp0(+v|1cib=A~H(x-`WNzGbj zIJiN3*OuzRk4ahHPe=s1vOcQnl379>v{-%8)c4UHG79tb5$;|ubZl0TF-;+zxozqq+y8G|K%l~3g{OkDHN3kO&75@)f zx|_HxAeBaUgv;Yq1S0%gW1!~1md#-=D07X`WXfuX?;0cUEPI_HMPVf#q@s3Orebbj zF!8FvK<|rgPiJ&kk)cMo1J<%HU_z{CFfVFCjxn)1*3l3m0XSHaBVqX1tZ5MfM@A&( zjN14TsiB0hUL3iglCs)rVaVhFfcc1|j!uZLQL7A-O|lt=DHqcK0J`nz`{R|rCKl@Y z|D5=+`{$XNJu_GCcFpdYy>hSXgYJ8~7upWa*B|<*?cg;2p)^(Vqcf$pgA4VC{*)L$ zQL5|zN38k(seH>bP<+E-{A|9i-)w|EZ*1*8;QLiWcUSm6_5*sqiwC&Q63TkPtGi(S zEp#3cEFr>3n6Pc2+w+{nz62D>FL6$s<0YQ>hJ8uF4jn6wDEjHVGOCit4KRn$Ep1<3 zwJeon*G3L1DA)+EJ_6(x+P2P+o)=@HU|#zwaiPBwLtuHid7)KjfLkT1RAY(Gp54q$)YnKySr|W;&e5ZLjGS_izZd=cM z;`n0XbSZIqKG8qXv%+wZ&C8pbruM)0?MZG$K=Jy;=FU=c=X~?t`Pkm&c*9)d-i7$S zkK>7@x=je{BVaZvDf=W{ z;P|9#NWq*oG^AyVd26)qvxrwa41!&~WrrYX=8O^9RMquWgAaH(p8HMbyw$29gP!NN zFiLG~LxWMuXG4?lel~;hmHERc$@C=|@+Fu@UICDVNJDYz2GvoUQM4o`J7LfSA4cWq z8!>tInAB__Cf9DA%Kh~6?aQ;dUmN#~IqA(sDO-YHZEk}5 zzTeT}x==>rr6Uk}!dnQg2H%aXpU&FQs(EiG6n6BY2VjOB%3d@K({*1Mqt|k5+@$fd zoLXF~+_d3Jo}YHYL~|1W@Y7h`jceDhEyh|)vDWFX`B-vJNIG`_iP7!uGGsS=Y_8AH zRYF64GC*F251}$4DGJgQP^OL&$g31{Olc3rs1vxhO_Lshlf8~;-@~$?{Zhug?;NJTQRjyL9J_R5-(&!FyJtaE8@= z8Q=4g%$hXh2mZ7M*wD@vy;{lJAxD#u-?i7*&-oO{>#7>{4FU|!T6Pr+~5rql~%1y0WrN z_PJ*?X*B{Br_VI)5O+EOrfBXvEs7;Gv9o0i&k&zL`Z>vrM; zVIsOzvwg9qy;Rda(=cC?p71Tj>TkSr{hg_+i?LKGmV%w;Y7Z@h4}To4ea zo8Ix`$j!Q$eG|Tw?I;v4pJ+bV^rRK>!HJH)ac+`@%AHm_u99eUBi$4n29RWI?jk(+ z`jS3Z^0Y#A!`hcAR+*iQ(W+snNS|+z>1mj;LzTC-Y7&l)_^i=_rVkTZj@s+rekt$m zm#U~Dr4$NzE$h79Q;O!~GP`5li}87@APv(h(0v_J|8-2sc}ut@hZAzCiZ{e!Tlk7w z_8cSwK&83S%m7$nF~$#2>nEt?0c!pPz5W0ldg5bn$CDt7J66Jo2|Z<^II)5Ntg_Hu Zak{MzA%J(SAOK|oU{4cGc(W74{{zd=sGtA< literal 0 HcmV?d00001 diff --git a/src/mini_projects/features/tictactoe/presentation/cli.py b/src/mini_projects/features/tictactoe/presentation/cli.py new file mode 100644 index 0000000..9ae988b --- /dev/null +++ b/src/mini_projects/features/tictactoe/presentation/cli.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from mini_projects.features.tictactoe.application.ai import TicTacToeAI +from mini_projects.features.tictactoe.domain.board import Board + + +def print_board(board: Board) -> None: + for row in [board.cells[i * 3 : (i + 1) * 3] for i in range(3)]: + print("| " + " | ".join(row) + " |") + + +def print_board_nums() -> None: + number_board = [[str(i) for i in range(j * 3, (j + 1) * 3)] for j in range(3)] + for row in number_board: + print("| " + " | ".join(row) + " |") + + +@dataclass +class HumanPlayer: + letter: str + + def get_move(self, board: Board) -> int: + while True: + raw_value = input(f"{self.letter}'s turn. Input move (0-8): ").strip() + try: + move = int(raw_value) + except ValueError: + print("Invalid square. Try again.") + continue + if move not in board.available_moves(): + print("Invalid square. Try again.") + continue + return move + + +@dataclass +class ComputerPlayer: + letter: str + difficulty: str + random_seed: Optional[int] = None + + def __post_init__(self) -> None: + self.ai = TicTacToeAI(self.letter, difficulty=self.difficulty, random_seed=self.random_seed) + + def get_move(self, board: Board) -> int: + return self.ai.choose_move(board) + + +def run_tictactoe_cli(difficulty: str = "hard", random_seed: Optional[int] = None) -> None: + board = Board() + x_player = ComputerPlayer(letter="X", difficulty=difficulty, random_seed=random_seed) + o_player = HumanPlayer(letter="O") + + print("Welcome to TicTacToe") + print_board_nums() + + letter = "X" + while board.is_empty(): + current_player = o_player if letter == "O" else x_player + square = current_player.get_move(board) + if not board.make_move(square, letter): + print("That square is already occupied. Try again.") + continue + + print(f"{letter} makes a move to square {square}") + print_board(board) + print("") + + if board.current_winner: + print(f"{letter} wins!") + return + + letter = "O" if letter == "X" else "X" + + print("It's a tie!") + diff --git a/sudoku.ipynb b/sudoku.ipynb index 08eaef0..b717299 100644 --- a/sudoku.ipynb +++ b/sudoku.ipynb @@ -1,158 +1,287 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 16, - "id": "eecd1f5f", - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n", - "[[3, 9, 1, 8, 5, 6, 4, 2, 7],\n", - " [8, 6, 7, 2, 3, 4, 9, 1, 5],\n", - " [4, 2, 5, 7, 1, 9, 6, 8, 3],\n", - " [7, 5, 4, 9, 6, 8, 1, 3, 2],\n", - " [2, 1, 6, 4, 7, 3, 5, 9, 8],\n", - " [9, 3, 8, 5, 2, 1, 7, 6, 4],\n", - " [5, 4, 3, 6, 9, 2, 8, 7, 1],\n", - " [6, 7, 2, 1, 8, 5, 3, 4, 9],\n", - " [1, 8, 9, 3, 4, 7, 2, 5, 6]]\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sudoku Solver + Generator\n", + "\n", + "Goal: combine constraint propagation and backtracking.\n", + "\n", + "Structure:\n", + "1. Imports/config\n", + "2. Pure helper functions\n", + "3. Solve/generate runner\n", + "4. Quick verification checks" + ], + "id": "7a8e5478" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\"\"\"Sudoku notebook: validation, constraint propagation, backtracking, and generation.\"\"\"\n", + "\n", + "from copy import deepcopy\n", + "import random\n", + "\n", + "SIZE = 9\n", + "DIGITS = set(range(1, 10))\n", + "\n", + "\n", + "def validate_puzzle(board: list[list[int]]) -> None:\n", + " if len(board) != SIZE or any(len(row) != SIZE for row in board):\n", + " raise ValueError(\"Puzzle must be 9x9.\")\n", + " for r in range(SIZE):\n", + " for c in range(SIZE):\n", + " v = board[r][c]\n", + " if v not in range(0, 10):\n", + " raise ValueError(\"Values must be 0..9.\")\n", + "\n", + "\n", + "def find_empty(board: list[list[int]]) -> tuple[int, int] | tuple[None, None]:\n", + " for r in range(SIZE):\n", + " for c in range(SIZE):\n", + " if board[r][c] == 0:\n", + " return r, c\n", + " return None, None\n", + "\n", + "\n", + "def candidates(board: list[list[int]], row: int, col: int) -> set[int]:\n", + " used = set(board[row])\n", + " used |= {board[r][col] for r in range(SIZE)}\n", + " br, bc = 3 * (row // 3), 3 * (col // 3)\n", + " used |= {board[r][c] for r in range(br, br + 3) for c in range(bc, bc + 3)}\n", + " return DIGITS - used\n", + "\n", + "\n", + "def propagate(board: list[list[int]], steps: list[str]) -> bool:\n", + " changed = True\n", + " while changed:\n", + " changed = False\n", + " for r in range(SIZE):\n", + " for c in range(SIZE):\n", + " if board[r][c] != 0:\n", + " continue\n", + " opts = candidates(board, r, c)\n", + " if not opts:\n", + " return False\n", + " if len(opts) == 1:\n", + " value = next(iter(opts))\n", + " board[r][c] = value\n", + " steps.append(f\"single({r},{c})={value}\")\n", + " changed = True\n", + " return True\n", + "\n", + "\n", + "def solve(board: list[list[int]], steps: list[str] | None = None) -> bool:\n", + " if steps is None:\n", + " steps = []\n", + " if not propagate(board, steps):\n", + " return False\n", + " row, col = find_empty(board)\n", + " if row is None:\n", + " return True\n", + "\n", + " ranked = sorted(candidates(board, row, col))\n", + " for guess in ranked:\n", + " board[row][col] = guess\n", + " steps.append(f\"guess({row},{col})={guess}\")\n", + " if solve(board, steps):\n", + " return True\n", + " board[row][col] = 0\n", + " steps.append(f\"backtrack({row},{col})\")\n", + " return False\n", + "\n", + "\n", + "def solve_copy(board: list[list[int]]) -> tuple[list[list[int]], list[str]]:\n", + " validate_puzzle(board)\n", + " clone = deepcopy(board)\n", + " steps: list[str] = []\n", + " if not solve(clone, steps):\n", + " raise ValueError(\"Puzzle is unsolvable.\")\n", + " return clone, steps\n", + "\n", + "\n", + "def generate_puzzle(removals: int = 45, rng_seed: int = 0) -> list[list[int]]:\n", + " rng = random.Random(rng_seed)\n", + " base = [\n", + " [5, 3, 4, 6, 7, 8, 9, 1, 2],\n", + " [6, 7, 2, 1, 9, 5, 3, 4, 8],\n", + " [1, 9, 8, 3, 4, 2, 5, 6, 7],\n", + " [8, 5, 9, 7, 6, 1, 4, 2, 3],\n", + " [4, 2, 6, 8, 5, 3, 7, 9, 1],\n", + " [7, 1, 3, 9, 2, 4, 8, 5, 6],\n", + " [9, 6, 1, 5, 3, 7, 2, 8, 4],\n", + " [2, 8, 7, 4, 1, 9, 6, 3, 5],\n", + " [3, 4, 5, 2, 8, 6, 1, 7, 9],\n", + " ]\n", + " puzzle = deepcopy(base)\n", + " cells = [(r, c) for r in range(SIZE) for c in range(SIZE)]\n", + " rng.shuffle(cells)\n", + " for r, c in cells[:removals]:\n", + " puzzle[r][c] = 0\n", + " return puzzle\n", + "\n", + "\n", + "sample = generate_puzzle(removals=50, rng_seed=5)\n", + "solved, steps = solve_copy(sample)\n", + "\n", + "# Quick verification checks\n", + "assert len(steps) > 0\n", + "assert all(all(v in range(1, 10) for v in row) for row in solved)\n", + "assert candidates([[0] * 9 for _ in range(9)], 0, 0) == DIGITS\n", + "\n", + "print(\"Generated puzzle first row:\", sample[0])\n", + "print(\"Solved first row:\", solved[0])\n", + "print(\"First 10 solving steps:\", steps[:10])\n", + "\n", + "\n", + "def find_next_empty(puzzle):\n", + " # finds the next row, col on the puzzle not filled -> -1\n", + " # return row, col tuple (or (None, None) if there is none)\n", + "\n", + " # keep in mind that we are using 0-8 for our indices\n", + " for r in range(9):\n", + " for c in range(9): # range(9) is 0, 1, 2, ... 8\n", + " if puzzle[r][c] == -1:\n", + " return r, c\n", + "\n", + " return None, None # if no spaces in the puzzle are empty (-1)\n", + "\n", + "def is_valid(puzzle, guess, row, col):\n", + " # finding out if row, col is valid\n", + " # returns True or False\n", + "\n", + " # for a guess to be valid, sudoku rules\n", + " # cannot repeat the number\n", + "\n", + " #row\n", + " row_vals = puzzle[row]\n", + " if guess in row_vals:\n", + " return False # if we've repeated, then our guess is not valid!\n", + "\n", + " #col\n", + " # col_vals = []\n", + " # for i in range(9):\n", + " # col_vals.append(puzzle[i][col])\n", + " col_vals = [puzzle[i][col] for i in range(9)]\n", + " if guess in col_vals:\n", + " return False\n", + "\n", + " # and then the square\n", + " row_start = (row // 3) * 3 # 10 // 3 = 3, 5 // 3 = 1, 1 // 3 = 0\n", + " col_start = (col // 3) * 3\n", + "\n", + " for r in range(row_start, row_start + 3):\n", + " for c in range(col_start, col_start + 3):\n", + " if puzzle[r][c] == guess:\n", + " return False\n", + "\n", + " return True\n", + "\n", + "def solve_sudoku(puzzle):\n", + " # solve sudoku using backtracking!\n", + " # our puzzle is a list of lists, where each inner list is a row in our sudoku puzzle\n", + " # return whether a solution exists\n", + " # mutates puzzle to be the solution (if solution exists)\n", + " \n", + " # step 1: choose somewhere on the puzzle to make a guess\n", + " row, col = find_next_empty(puzzle)\n", + "\n", + " # step 1.1: if there's nowhere left, then we're done because we only allowed valid inputs\n", + " if row is None: # this is true if our find_next_empty function returns None, None\n", + " return True \n", + " \n", + " # step 2: if there is a place to put a number, then make a guess between 1 and 9\n", + " for guess in range(1, 10): # range(1, 10) is 1, 2, 3, ... 9\n", + " # step 3: check if this is a valid guess\n", + " if is_valid(puzzle, guess, row, col):\n", + " # step 3.1: guess valid , place it into row,col\n", + " puzzle[row][col] = guess\n", + " # step 4: call res\n", + " if solve_sudoku(puzzle):\n", + " return True\n", + " \n", + " # step 5: it not valid or if nothing gets returned true, then we need to backtrack and try a new number\n", + " puzzle[row][col] = -1\n", + "\n", + " # step 6: if none work, unsolveable\n", + " return False\n", + "\n", + "if __name__ == '__main__':\n", + " example_board = [\n", + " [3, 9, -1, -1, 5, -1, -1, -1, -1],\n", + " [-1, -1, -1, 2, -1, -1, -1, -1, 5],\n", + " [-1, -1, -1, 7, 1, 9, -1, 8, -1],\n", + "\n", + " [-1, 5, -1, -1, 6, 8, -1, -1, -1],\n", + " [2, -1, 6, -1, -1, 3, -1, -1, -1],\n", + " [-1, -1, -1, -1, -1, -1, -1, -1, 4],\n", + "\n", + " [5, -1, -1, -1, -1, -1, -1, -1, -1],\n", + " [6, 7, -1, 1, -1, 5, -1, 4, -1],\n", + " [1, -1, 9, -1, -1, -1, 2, -1, -1]\n", + " ]\n", + " print(solve_sudoku(example_board))\n", + " pprint(example_board)" + ], + "execution_count": 16, + "outputs": [ + { + "output_type": "stream", + "text": [ + "True\n", + "[[3, 9, 1, 8, 5, 6, 4, 2, 7],\n", + " [8, 6, 7, 2, 3, 4, 9, 1, 5],\n", + " [4, 2, 5, 7, 1, 9, 6, 8, 3],\n", + " [7, 5, 4, 9, 6, 8, 1, 3, 2],\n", + " [2, 1, 6, 4, 7, 3, 5, 9, 8],\n", + " [9, 3, 8, 5, 2, 1, 7, 6, 4],\n", + " [5, 4, 3, 6, 9, 2, 8, 7, 1],\n", + " [6, 7, 2, 1, 8, 5, 3, 4, 9],\n", + " [1, 8, 9, 3, 4, 7, 2, 5, 6]]\n" + ] + } + ], + "id": "eecd1f5f" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "4d30956f" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "08013dff" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" } - ], - "source": [ - "from pprint import pprint\n", - "\n", - "\n", - "def find_next_empty(puzzle):\n", - " # finds the next row, col on the puzzle not filled -> -1\n", - " # return row, col tuple (or (None, None) if there is none)\n", - "\n", - " # keep in mind that we are using 0-8 for our indices\n", - " for r in range(9):\n", - " for c in range(9): # range(9) is 0, 1, 2, ... 8\n", - " if puzzle[r][c] == -1:\n", - " return r, c\n", - "\n", - " return None, None # if no spaces in the puzzle are empty (-1)\n", - "\n", - "def is_valid(puzzle, guess, row, col):\n", - " # finding out if row, col is valid\n", - " # returns True or False\n", - "\n", - " # for a guess to be valid, sudoku rules\n", - " # cannot repeat the number\n", - "\n", - " #row\n", - " row_vals = puzzle[row]\n", - " if guess in row_vals:\n", - " return False # if we've repeated, then our guess is not valid!\n", - "\n", - " #col\n", - " # col_vals = []\n", - " # for i in range(9):\n", - " # col_vals.append(puzzle[i][col])\n", - " col_vals = [puzzle[i][col] for i in range(9)]\n", - " if guess in col_vals:\n", - " return False\n", - "\n", - " # and then the square\n", - " row_start = (row // 3) * 3 # 10 // 3 = 3, 5 // 3 = 1, 1 // 3 = 0\n", - " col_start = (col // 3) * 3\n", - "\n", - " for r in range(row_start, row_start + 3):\n", - " for c in range(col_start, col_start + 3):\n", - " if puzzle[r][c] == guess:\n", - " return False\n", - "\n", - " return True\n", - "\n", - "def solve_sudoku(puzzle):\n", - " # solve sudoku using backtracking!\n", - " # our puzzle is a list of lists, where each inner list is a row in our sudoku puzzle\n", - " # return whether a solution exists\n", - " # mutates puzzle to be the solution (if solution exists)\n", - " \n", - " # step 1: choose somewhere on the puzzle to make a guess\n", - " row, col = find_next_empty(puzzle)\n", - "\n", - " # step 1.1: if there's nowhere left, then we're done because we only allowed valid inputs\n", - " if row is None: # this is true if our find_next_empty function returns None, None\n", - " return True \n", - " \n", - " # step 2: if there is a place to put a number, then make a guess between 1 and 9\n", - " for guess in range(1, 10): # range(1, 10) is 1, 2, 3, ... 9\n", - " # step 3: check if this is a valid guess\n", - " if is_valid(puzzle, guess, row, col):\n", - " # step 3.1: guess valid , place it into row,col\n", - " puzzle[row][col] = guess\n", - " # step 4: call res\n", - " if solve_sudoku(puzzle):\n", - " return True\n", - " \n", - " # step 5: it not valid or if nothing gets returned true, then we need to backtrack and try a new number\n", - " puzzle[row][col] = -1\n", - "\n", - " # step 6: if none work, unsolveable\n", - " return False\n", - "\n", - "if __name__ == '__main__':\n", - " example_board = [\n", - " [3, 9, -1, -1, 5, -1, -1, -1, -1],\n", - " [-1, -1, -1, 2, -1, -1, -1, -1, 5],\n", - " [-1, -1, -1, 7, 1, 9, -1, 8, -1],\n", - "\n", - " [-1, 5, -1, -1, 6, 8, -1, -1, -1],\n", - " [2, -1, 6, -1, -1, 3, -1, -1, -1],\n", - " [-1, -1, -1, -1, -1, -1, -1, -1, 4],\n", - "\n", - " [5, -1, -1, -1, -1, -1, -1, -1, -1],\n", - " [6, 7, -1, 1, -1, 5, -1, 4, -1],\n", - " [1, -1, 9, -1, -1, -1, 2, -1, -1]\n", - " ]\n", - " print(solve_sudoku(example_board))\n", - " pprint(example_board)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d30956f", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08013dff", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adc1c7069af996188bf2cb4c3573584558ec94d9 GIT binary patch literal 693 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 literal 0 HcmV?d00001 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..efec060 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SRC_PATH = PROJECT_ROOT / "src" + +if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + diff --git a/tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc b/tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9a49212f4d067dab6877f05607de0db3c894df3 GIT binary patch literal 4480 zcmd5<-ESOM6~FVbpR-;&c1hyY6x(qi1IesE8=PiCTTxnEWSUGCEG=R**&RER?#`_4 zopllek|lTu5)yf6q9Qgr|#aWh126n1|-a%9Q;)vQP9&cpX^O}DYSD)g-nxJ6ZDKbRS_?~L5RC> zHngdKNJv!<6>>El<@)I~(X$l^de5zmE{aQ`t)8!DBb%#y=OZmh`{TX@1@-3#Mkx&1 zqBae4T?$ti&CNVM(5m?Pt-de7ocb#{+**jnIMPy#EOkYb&cnVqvR=@KD)OwrwN&+3 zG@nFYqfuEug_WhEzM|9u-zy7_P!?MA8$>Ox`l*`Rc07tFx{u<4;Ac(GvzjfB`Dt*Q zHgyL41Xnr+{{uKqa#^@fu7RhN#j=_(2r(7d)r2qg=d>ZK zsa1-{x{vjK4u^NC2_7rDZ>xfz|AEL(miGD-s@duVn^sHQy-5`bgI2kb+@4I4UZi_3qnUgJPO?arqMSsGolsb_6%Wr2a7jmeVk()uJ`G*)pl+7ogg>-nJW_?SP{M3-(VM?1tNJ z)VZm#(QvPu4Z|^;mY*}2xo%+(e(@DPwCy_lMEx;i-K9;_GpyUKhUu6d)TkLgZ(qS- zf~n3WmBpm85GX+}FD8|5QgAxL5ZfmC_cFyu@u!YFLwXMEa@hD?5O7|5BPTk??*4T9 zjlZPw_g?9yj(;M`spsC+|3M%*kVyIjFZf%56rS%X6T9-vj?6_@nRzUSzlZY7t}+p; zLFSP>bN5?uS+}o2MQq)}{$isz^Lu%QehDTx5TY@Z{|4gZej@0q2=M$XL13LnLNFgZ8&j8ADwAWD@fO2aJ0J#7+ImIbZbiI8%u6+btt_BqdAGCx5 zZYcs#FdD{v6!QxZ&6(hWXR1iY!3v{$VJjK(2n=vH5;gD34IAPePmq2E#UzR;6w@e9 zqnJT)2E|uFs3|%NNiqcIDFS{n^cLyYp@boz@W=}SZ;S`tAPu3&6mT)@=ODJp$Hmci zF28r~w?c1hzE@n>etqxccxUe3=8mv4*V+6)_+V~lbN9HmD{COTiuPCze-CBt?uA$j zGLHtzx_u2Qp!pNlph72~6Hy<+kt8DuNfM_;5|W;)VKl)q;?EIu;eVt*DNcXV1N0~J z1&p7})5p(|mVNq@jpMKh7WY>{^@NNRWj;{Lox>W;i>A95czX02~ zH5^gEkS?l{FLTN=(zKm6;FSR^$Oe7_&nVAr*|i9G&;54}reOyIG|ZPhc$d+WFe05m z@nsOI)CYTtz`m8n=URCIV0{*QUyK+YWD*Q795_MrfWlF{DB$`)z^C8_9Q7CZI0MkX z9I(A*RiSvl&}9@)iTuGBIeH1(AmUSWJgTBVxToI$k-Qf|HWo0rrGPaCJZ)~4;T^~C z1{A^lFwO6Wy6Ks<1~f8E2bei^J-6mIj2l*?Wl{PnG{d`#i9m1Qx=>DZe)PLncfa$) zuCliM?LB4q*UP_L2I|!*aPb8Hy1N{cryzs$YcVcE`W0$e5gG&g>J(y|I0ME0`1Gz^ z`c=6z-cwFPZs#@^U8VF`4u22j(ynqkR)fqVY!sJu`x;bq#)FDpq9NpWZpZd<<_~g- z-&SzZBT}rYML!#{a(;>XlU@aopW)Kp9#yjBls_Y=i9jN{4u3P zk0mXNl<={nnXOjCu5rSyMQ^6Ssn8esV*4qv1GW{+X q;lqw0<@iJr1mTZ_{DSPW}xS)>)hY literal 0 HcmV?d00001 diff --git a/tests/features/advanced_python/test_patterns.py b/tests/features/advanced_python/test_patterns.py new file mode 100644 index 0000000..35391ba --- /dev/null +++ b/tests/features/advanced_python/test_patterns.py @@ -0,0 +1,30 @@ +from mini_projects.features.advanced_python.application.patterns import ( + LearningTask, + describe_size, + retry, + sliding_window, +) + + +def test_retry_succeeds_after_failure() -> None: + attempts = {"count": 0} + + @retry(attempts=2) + def sometimes_fails() -> str: + attempts["count"] += 1 + if attempts["count"] == 1: + raise ValueError("temporary") + return "ok" + + assert sometimes_fails() == "ok" + + +def test_sliding_window() -> None: + windows = list(sliding_window([1, 2, 3, 4], 2)) + assert windows == [[1, 2], [2, 3], [3, 4]] + + +def test_dataclass_and_protocol_helper() -> None: + task = LearningTask(name="decorators", minutes_estimate=30, topic="advanced-python") + assert task.name == "decorators" + assert describe_size([1, 2, 3]) == "size=3" diff --git a/tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc b/tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1e7311d90db1dfde52b6c5ec9eccf5109116f8d GIT binary patch literal 4678 zcmb7IU2Igx6`r|!_s6^T8XFt1N$kZC;HvT({ELYVA%@VJ%7MMCMB=Ke%kDK^XZPOK z%-v8^HF86x(56z5s#K^-BvcWpY5TxarM^{a-|LM-jINMci6`Eim@1`DJ!kITpItU` zGFqRRGc#xA-1+&wGv4m#h!aTT|9nz>93tc~DsBQ@r0st|_90P;N-q#bMJ~;UxRfIE zG7G1imI#Z8mMDutE?tPtx3RYQIE&9GSRzG!6wU0bj~B=??E$4;R3mDrpRC9eb)9v5 zCtJC}HVLDJv4(YCqe}D%j=7+O?0xxV)8u+CZyNb3W4e)F&6Ul9Ub1&B=~jL@cfDvW z=a_D>RlAE-joekUY83QB&eHf>JYRQBWKobe;ct7L67u87L6XA7qN;R^mf=$ty}qjK zm&fI7WSk1uVk@#i7Tv`rSpqIc)!20C2e7cqQ2#dlFtiYECk9p9%mZ2JW@)i) zLnKQlUycPuy8;c(B=%IGx`BMX0>3N2A(*Mc3>EK4s}EY2h_w`tCfz`>|8BVYeq zF(^9z8~EG42;vlVj=^QB(phO%3PYVL4R;X2;ockp4O^VrlUE4Q60lI-XllrJ=$b6t z2rn=^XHT%tjV^S@WExvdEosG4mFaf0qOF!pt)Ni5JySKTVp%`On8_50;V#=@ZfTaz z@dHw8wmy>OI^!dH^_p(zyjUK&$jlWzZ}E|mt}$b{vT7}x#>fwaa?w%19NL$34L)7x zBN|^d^1=Hvf*o>|Vnr_%4Lx01wWH!H>C8SUnwl?GPJdfBtg2Qjt>$<&pV##QPtTOh zyjJ3;(skQi;4vQs@!uz8jXaEX+*;X)_1(Etiw!{jFtO`)e=YICUAmDt@ThxWeQ|y7=XB@ZW=E1|%j)B8wmF4v(xB#$5QM}njoijYhpVI$Hv$G1l(f!iV%IYzN zr)FnUSR7M0ZZ)r#EJd)+VNLl0dX}nW_RV_ zdrAN$6_z*46%9749h!4jtHqL4G++kji|u_mzHC-Y1<^Hktz=%+N;yL->vmg?YfCzg zU?j`es@nPhnL zI3Bl@NUyL?97*}so5=9~}tZ|8((ole_Tx-DnxSIG*ULqU$Ythn6e^ov2 zkd1t+mfrbR;8kMB-X0t6*wce_wLL96Or>_H$!xgJTam@x?otKD>T&D*9C$0@uAG}Y zyk$thU317?=jp>~nvo(sgD zql*{7jw>LLEgmI%KPdmIeCNzYa$qwyupS$D*tz$EcYgWKM(5yqY!Hy3ZSWaKpnA7| zjDAFq%OCfSQSgmwHoa=oJi|Q!y66gqiKSw>Xkn)_HhtBmbF5c%x^b*d8S$Ui3n_<1 zfKZNN&2fb*@t?WeGKt*6O=jr@WFzDluE-2yWaN|Mtgl|0Y@nMH4&9tk5)AGDVYupS z5(R!adlAI|3b@CFrBEC|F@)kE2$vkDu;LJiASp})l?GCn3NplWfE=7nviG#!A9fmM z08k+O?;vnbwqz-sx)<6ako`II6tkx>(tG&#=KbsI8MW4Pc{6!=?fj!e*X<+kpI(=T ze`kWYW7ZPGn{xfPArId=?P*TofjsHX+)qn}p4kk=1ZNlPPBSD0{ z1QP_W{8&(#WO!DB_x^+vxjHf9@namh)Cm!t2=PS$0^jtqX7kV;5r@AA0%G|?>2-PL z^Tffs^Bai~DBhnIqL!H1l None: + state = {"attempts": 0} + + async def fetcher(_: str) -> str: + state["attempts"] += 1 + if state["attempts"] == 1: + raise RuntimeError("temporary failure") + return "payload" + + result = await fetch_with_retry(fetcher, "resource", retries=2) + assert result == "payload" + + +@pytest.mark.asyncio +async def test_run_bounded_tasks() -> None: + async def fetcher(resource: str) -> str: + await asyncio.sleep(0) + return resource.upper() + + results = await run_bounded_tasks(["a", "b", "c"], fetcher, concurrency_limit=2) + assert sorted(results) == ["A", "B", "C"] + + +def test_choose_concurrency_model() -> None: + assert choose_concurrency_model("io_bound") == "asyncio" diff --git a/tests/features/backend_api/__pycache__/test_app.cpython-313-pytest-9.0.2.pyc b/tests/features/backend_api/__pycache__/test_app.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d774c2841420fdeda9d28daaf837bb55ec72a54 GIT binary patch literal 9879 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 None: + client = TestClient(create_app()) + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_create_and_list_notes() -> None: + client = TestClient(create_app()) + create_response = client.post("/notes", json={"title": "Module 1", "body": "Decorators"}) + assert create_response.status_code == 201 + + list_response = client.get("/notes") + assert list_response.status_code == 200 + payload = list_response.json() + assert len(payload) == 1 + assert payload[0]["title"] == "Module 1" + + +def test_world_bank_refresh_and_status_endpoints(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_refresh(**kwargs): + _ = kwargs + return { + "kpis": {"observation_count": 3.0}, + "dashboard": {"kpis": {"observation_count": 3.0}}, + "processing_strategy": "pandas_local", + "record_count": 3, + "freshness": {"served_from_cache": False, "is_stale": False}, + } + + def fake_status(): + return {"has_cache": True, "is_stale": False} + + monkeypatch.setattr(app_module, "get_or_refresh_world_bank_dashboard", fake_refresh) + monkeypatch.setattr(app_module, "get_world_bank_freshness", fake_status) + + client = TestClient(create_app()) + refresh_response = client.post("/world-bank/refresh", json={}) + assert refresh_response.status_code == 200 + assert refresh_response.json()["record_count"] == 3 + + latest_response = client.get("/world-bank/latest") + assert latest_response.status_code == 200 + assert latest_response.json()["processing_strategy"] == "pandas_local" + + status_response = client.get("/world-bank/status") + assert status_response.status_code == 200 + assert status_response.json()["has_cache"] is True diff --git a/tests/features/big_data/__pycache__/test_chunked_processing.cpython-313-pytest-9.0.2.pyc b/tests/features/big_data/__pycache__/test_chunked_processing.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97752ca9ec48e245b7649a6b4d9ddba265ab0972 GIT binary patch literal 3934 zcmeHK&2Jk;6rWk|+KxBrryy-9V3M}#(#DSS*^s&_qy$K-P`a`dDTmd@-ZaYc0d`DCv=@XpS>k{cDkM(4nf>sZwxS#Wfx241d7m?HXWqX3 zd4FIagW&q$n~u4S5&Dq{;Uxsj{yV_jLn>17RYWi!g@ptavS>jhQkGeStI35FNiC#F z8e~aLoIp24(g&Q#xl9!+$qt)vQ4u`3yw?>yjTjeK&mcu7P zH^eZeI}SnKC2<0)$)({5VKVSgQ_IQF=LXB-57!Wy3^cPaR$lK}h3{=|zz|xF6q7;b zVUGHVp6LF_F=Cz;{UL@NyHEDis1gd-`cqH*+@8X7dnQ6pHJy`2U_UKM?Nc))X%urQ zxqL8u#8`fZLS1(pLZxIlS2!9_kwvHl_+j`7MeNK~ks>cDQipADhwV*=6;FqqX-5J= zQa}{!Wq2WK)Z8>WEQN>oH=$Tm@aC1%XPu2Hg=Rhtm~1i2xKV|kW#gDa&u6>AMW&lj zsFL|CTl76=6iV2lisU6Aj*&4&CK#DwWM=cAPN_j$K69a_8Ugom1;lj(I z>BKv%!Mjn@h~bd3U3YY1c>Q43RvKo_H7)8TYc_aL@0do{>_)A^UA1b>UeRlsrPmFw zPow&(!DjFdyv^4&ZR=e^Y~mf&R&4^Wpk{pH)O1UC!J?!*nXTZbScnVbabYqpT!;(P zev1Coh0z{qjItBR`j@wwFTp<0ywRu{mO;&W{yMR58fBN}Ylcp&5!i?|+sa?#!gXI@ zTSN1!hVC|qLGvqSRjcT(p66Q@HW-K4WrI@Fs^**x?@*5onh!Y*S+Gp#eIR#`H+ZHg z&VE$@^5yK^+bww#xJO4gY00y@VsLMZvrTz25`AWe1x316R|3P`+rFXAW{=ot4p2Wj z5{1SYSb85)Lp|M)Wz4(b*HFyRnW6f-U9kJETUZtBH%bEUgyUhCsqFT2nWuNcL@5EC zP+U3^>x9yOpc6{5{_!t#LN%Eac}Ek~l$tJyL4T9}LVuIOz7~$X{mmIu`<{8)nHQ(c z-j(ozuf^Z`-TOyQTYlzg_rEyp@TA%Je>iQxEdz_@))yFg{f=%`bgJ?1IvIcm1XwZ} z;0zy@0N{8Mmc$^)G(Zs6-W8EBa6=fNB8P!6^fA48Acj5|S~&_9^YYbe$U6iJuWNS0 z)$CQRY!kyoKRlMa!xVtWx>hj(qMIuX*QfwgVGEF#7MM}Tb>=6u?c=XS;5-Uhuq5{W*u!{Z`lD<* None: + assert chunked_sum([1, 2, 3, 4, 5], chunk_size=2) == 15 + + +def test_estimate_scale_strategy() -> None: + assert estimate_scale_strategy(100_000) == "pandas_local" + assert estimate_scale_strategy(5_000_000) == "dask_out_of_core" + assert estimate_scale_strategy(100_000_000) == "spark_distributed" diff --git a/tests/features/capstone/__pycache__/test_capstone_pipeline.cpython-313-pytest-9.0.2.pyc b/tests/features/capstone/__pycache__/test_capstone_pipeline.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b2102a8140d9b55b142bc2517d59b7e20376299 GIT binary patch literal 2573 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 literal 0 HcmV?d00001 diff --git a/tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc b/tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94f4933f0db87d21ccb614f7c85a776dce562a0b GIT binary patch literal 5045 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 Gss9D*iCLHc literal 0 HcmV?d00001 diff --git a/tests/features/capstone/test_capstone_pipeline.py b/tests/features/capstone/test_capstone_pipeline.py new file mode 100644 index 0000000..9a8bfd9 --- /dev/null +++ b/tests/features/capstone/test_capstone_pipeline.py @@ -0,0 +1,30 @@ +from mini_projects.features.capstone.application.pipeline import run_capstone + + +def test_capstone_returns_combined_outputs() -> None: + records = [ + { + "date": "2026-01-01", + "country": "United States", + "country_iso3": "USA", + "indicator": "GDP growth (annual %)", + "indicator_id": "NY.GDP.MKTP.KD.ZG", + "category": "growth", + "value": 2.1, + "fetched_at": "2026-01-10T12:00:00+00:00", + }, + { + "date": "2026-01-01", + "country": "Canada", + "country_iso3": "CAN", + "indicator": "Inflation, consumer prices (annual %)", + "indicator_id": "FP.CPI.TOTL.ZG", + "category": "prices", + "value": 2.9, + "fetched_at": "2026-01-10T12:00:00+00:00", + }, + ] + result = run_capstone(records) + assert result["kpis"]["observation_count"] == 2.0 + assert result["processing_strategy"] == "pandas_local" + assert "dashboard" in result diff --git a/tests/features/capstone/test_world_bank_client.py b/tests/features/capstone/test_world_bank_client.py new file mode 100644 index 0000000..fccad64 --- /dev/null +++ b/tests/features/capstone/test_world_bank_client.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import pytest + +from mini_projects.features.capstone.infrastructure.world_bank_client import ( + WorldBankClient, + WorldBankClientError, +) + + +class _StubResponse: + def __init__(self, payload: list[object], status_code: int = 200) -> None: + self._payload = payload + self.status_code = status_code + + def raise_for_status(self) -> None: + if self.status_code >= 400: + raise RuntimeError("http error") + + def json(self) -> list[object]: + return self._payload + + +def test_fetch_indicator_observations_handles_pagination(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[int] = [] + + def fake_get(url: str, params: dict[str, object], timeout: float) -> _StubResponse: + _ = url, timeout + page = int(params["page"]) + calls.append(page) + if page == 1: + return _StubResponse( + [ + {"page": 1, "pages": 2}, + [ + { + "country": {"value": "United States", "id": "USA"}, + "indicator": { + "value": "GDP growth (annual %)", + "id": "NY.GDP.MKTP.KD.ZG", + }, + "date": "2024", + "value": 2.5, + } + ], + ] + ) + return _StubResponse( + [ + {"page": 2, "pages": 2}, + [ + { + "country": {"value": "Canada", "id": "CAN"}, + "indicator": { + "value": "GDP growth (annual %)", + "id": "NY.GDP.MKTP.KD.ZG", + }, + "date": "2024", + "value": 1.9, + } + ], + ] + ) + + monkeypatch.setattr("httpx.get", fake_get) + client = WorldBankClient() + observations = client.fetch_indicator_observations( + indicator_id="NY.GDP.MKTP.KD.ZG", + countries=["US", "CA"], + ) + + assert calls == [1, 2] + assert len(observations) == 2 + assert observations[0].country == "United States" + + +def test_fetch_indicator_observations_retries_and_raises(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get(url: str, params: dict[str, object], timeout: float) -> _StubResponse: + _ = url, params, timeout + raise RuntimeError("network fail") + + monkeypatch.setattr("httpx.get", fake_get) + client = WorldBankClient(retries=2) + with pytest.raises(WorldBankClientError): + client.fetch_indicator_observations( + indicator_id="NY.GDP.MKTP.KD.ZG", + countries=["US"], + ) diff --git a/tests/features/capstone/test_world_bank_refresh.py b/tests/features/capstone/test_world_bank_refresh.py new file mode 100644 index 0000000..4d9cbbd --- /dev/null +++ b/tests/features/capstone/test_world_bank_refresh.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from datetime import UTC, datetime + +from mini_projects.features.capstone.application.pipeline import ( + get_or_refresh_world_bank_dashboard, + get_world_bank_freshness, +) +from mini_projects.features.capstone.infrastructure.cache_store import InMemoryCacheStore +from mini_projects.features.capstone.infrastructure.world_bank_client import ( + WorldBankObservation, +) + + +class StubWorldBankClient: + def __init__(self) -> None: + self.calls = 0 + + def fetch_indicator_observations( + self, + indicator_id: str, + countries: list[str], + per_page: int = 1000, + ) -> list[WorldBankObservation]: + _ = countries, per_page + self.calls += 1 + return [ + WorldBankObservation( + country="United States", + country_iso3="USA", + indicator="GDP growth (annual %)", + indicator_id=indicator_id, + date="2024", + value=2.3, + fetched_at=datetime.now(tz=UTC), + ) + ] + + +def test_refresh_uses_cache_when_fresh() -> None: + store = InMemoryCacheStore() + client = StubWorldBankClient() + first = get_or_refresh_world_bank_dashboard( + force_refresh=True, + ttl_seconds=3600, + countries=["US"], + indicators=["NY.GDP.MKTP.KD.ZG"], + cache_store=store, + client=client, + ) + second = get_or_refresh_world_bank_dashboard( + force_refresh=False, + ttl_seconds=3600, + countries=["US"], + indicators=["NY.GDP.MKTP.KD.ZG"], + cache_store=store, + client=client, + ) + + assert client.calls == 1 + assert first["freshness"]["served_from_cache"] is False + assert second["freshness"]["served_from_cache"] is True + + +def test_freshness_status_without_cache() -> None: + status = get_world_bank_freshness(cache_store=InMemoryCacheStore()) + assert status["has_cache"] is False + assert status["is_stale"] is True diff --git a/tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc b/tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e57fdc97125e99a26955d6f4d7930f24c0f3fa7f GIT binary patch literal 4225 zcmd^C&2QUA79UE~CnecV;y6y7L{4JIY!XTSNLr+^>ulo9*6zA)XkptLSb~vgg^f%K zLn>MO!oXELcO_1Y<_<&_O&N3BiqtSG9 z^l2=9)gojq?%#JW>2tm7d7|`{ z<1>QN|K7q(s{fgNq;T>szXN23Z^(KG<1l>2`>_ZFL@-Q_xgKr^Eu@tMn@U)Vw8BfV^kyZ zspYNoRB7t^)zTzP7Q1g*5ADM$1nCgVmn<@|3Wb6tgL0AZ$^eU7w5n4(bpI5K)u49R zhtKeS{ER}z>BA}1;s?&Mj^$`|)my0sDngz&=XZtgBeS!+!q%~|iN@xnZ0BcZ^T^J~ z_HKklL5F1K`eatF^EtFc44!aTR25tl@m2;H5+=BN$^zFJwPceOoxRjS8$(lEiAldr(o}Sf3Uw6 z%Mc5@s5fi6soTazafMnR>J`T>)^&}VSK*8gEVKAF7gk&WPi+_1bPZk zDi4cpJWQ>=X&ZK-vB`$GhI_z4xmsPXTaW1^Y(s*dy#ViYn>-&l^YpbZ>}Og#xzha^ zPoKTpPQLRZA@;?$?|zde*)vc6(#ec%FMK5pJ&ARsv)gyR%8l-fb#fEicb`jrpZ2w- zd`pyf7C323@`32hJyC8+c~{&I@g4LIyaEQ)U05&Imd>@r@ttW-+S2%e=*>NGyd|A; z#r+W9LGQpTU_jlKj(u|TWKhW2bGh*3#|H(@v=wamtpl3ht**(TjEM?GJ+ZN2Bhx)*od9x8S~2jd&U5? zj=0b|<3j6Jwrm=XUd<~G;3+^>S-G_}c=ygq9&q8Y^B_N=nPyX~=f~u&f#vrLU@820 zX?3Nrbf@saT^4t}YXAHa2btd*2Gr(DPnK3or5k)+=2T8n>?T>FVl_<%8hrS7z!}xB zt!b7DI(?Q2Sk~9jpPiHvp`tZKZtwnA-ljLhAD^o2E+9G20(r6{V8@M6!Vkjk&GfakK_W9i%7G!qO6`*a(NgUci2gg?|7S+^k*WSok7N21mO?(#4i| z`Dv7swsiSG^yZ#;xg}k6#r+W9LGQpTU_gy9>B8=%o!c!DNLvD+^5&j+iNh|6`ysxA zj)7Oefcob!IrsGSf4tXvb*?4O0c}fj2ckFkK!ae3;(mzlaL>RkV8DlBKPg98oa)fU zn9Fh>OWto9b;mGmmcb?~kcL&a(JQ!Z7c^@Hsz0XiJ+f4w!oH2`c!sd+8-{794X>9f z_`Oo0*DDpYMx%~@uyH*=xf j$TlYYi>M$7|03ig@*PPC5l+52krAf8J$puYli&Xz#l38$ literal 0 HcmV?d00001 diff --git a/tests/features/data_stack/test_analysis.py b/tests/features/data_stack/test_analysis.py new file mode 100644 index 0000000..30c0e52 --- /dev/null +++ b/tests/features/data_stack/test_analysis.py @@ -0,0 +1,55 @@ +from mini_projects.features.data_stack.application.analysis import ( + compute_kpis, + compute_indicator_kpis, + indicator_by_category, + load_indicator_frame, + load_sales_frame, + revenue_by_category, +) + + +def test_data_analysis_flow() -> None: + records = [ + {"date": "2026-01-01", "category": "books", "revenue": 100, "orders": 2}, + {"date": "2026-01-02", "category": "games", "revenue": 200, "orders": 4}, + {"date": "2026-01-03", "category": "books", "revenue": 50, "orders": 1}, + ] + frame = load_sales_frame(records) + kpis = compute_kpis(frame) + grouped = revenue_by_category(frame) + + assert kpis["total_revenue"] == 350.0 + assert kpis["total_orders"] == 7.0 + assert grouped.iloc[0]["category"] == "games" + + +def test_indicator_analysis_flow() -> None: + records = [ + { + "date": "2024-01-01", + "country": "United States", + "country_iso3": "USA", + "indicator": "GDP growth (annual %)", + "indicator_id": "NY.GDP.MKTP.KD.ZG", + "category": "growth", + "value": 2.1, + "fetched_at": "2024-01-10T00:00:00+00:00", + }, + { + "date": "2024-01-01", + "country": "Canada", + "country_iso3": "CAN", + "indicator": "Inflation, consumer prices (annual %)", + "indicator_id": "FP.CPI.TOTL.ZG", + "category": "prices", + "value": 2.9, + "fetched_at": "2024-01-10T00:00:00+00:00", + }, + ] + frame = load_indicator_frame(records) + kpis = compute_indicator_kpis(frame) + grouped = indicator_by_category(frame) + + assert kpis["observation_count"] == 2.0 + assert kpis["country_count"] == 2.0 + assert grouped.iloc[0]["category"] in {"growth", "prices"} diff --git a/tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc b/tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dae40e770d92b5d5859410826e75d957d2c0e267 GIT binary patch literal 6252 zcmeHL&2QVt6(^<9CvD44oU~3-N3r5Xm^PB-*iN#kgH1QdZXL(ngb|Qk9|*>x9i=j< z4CQX@U;!5`PzOD%fkgrJ=4(&=CwkahrIi&KM1dCQNw*f>qCijk-jG95{#ZAQ0PP_J z@#f9Uo0&K74f%U-c6)o10z8-g+$nt=7KFcJg+C#e*?kDi?*v6q#Jd6&Ss9uMTcNZt zLue$8J)yhNnHY`D#A$pcK@&4cnuPYK5h84mm>n#;yN@IPYc34Gd@Z@=<($^2Wts@ExwX=vTIZym-WlwsGo_TlfTBn z(%#OGl=OCfq-2=ut&l9a1XeM~`~cT6F!~s8^Gnd5@X@@~;m}QshBz%&GKYF6a+r61 zD3}FqKjtYomTx_U76)7J_f+&XJQd|0;-&8?BLv=>1aA%czU3TzONV-kc_{YQLyoNu z^w1momX-uLK4(WOtJNo_vG$>6SvswzM=w-Xr(`Sn!e!FatZAG#0pm7}$yV0VHM;O5 z`Pl`Oo{%j$`I$|s@HdCQdbqMWhGZPc1d=J6;A(5L?plWV2G(1<_uVwFYP4#ppO&iK z>dO*Egs`I}1MOi)oh{lu?^RaS$1A0B6*}x_*<8@dmYsywD#DXS?eDAB6LY0pRHQdQ!td&(mTLxrPEp1W9X1nhuGcy3)ZAwjQA6FMmx~x@I{duLV8Cn&3Y{~MKlVdc2 z`bfCGv(tRAayeL;bSlo2SAsom#p$_fAylTF$No51fSp=7ePu~EbgQ(S`-qxP^@XaH zD}#HDOU?<&&9cfz?6~A`(^hU#*QzU2w{k^GV@;}7UNR}zblJ*QR_zgH8$1ty-)K(N zv8JjSMYTfp#nN+{232r})(T+oy6_h%_1mfSTRSBFo4dcfyBXgmLpxIH^Xo6K*GYQw zIgpL(o6nn48n~8}Zj-LJ0o@Z`TcFm$aZX1Q=keJ3zB#r@dM9=K^A|5))X7L~3dqKb z+Eg<&0^GKQv?YzSN!Qx|p{mywsI_pk)6vA)JhqY#WltdK65gCnkx5mJbm9}ym9xU_l8!}32t)D=;p|A$x}4a7MA zgQ)As)x8KyiDe@!K8w5t>PI-*^KuXBPaGPfp+Dn?`4i%_#V+3_-uf25%kDVsJNP#C z8orHjpYpQXw?-JEx;qdzBr+z37J# zKjL8s#uLdE%nQ{;NDSo&#e{}RNHA%ly+AOuxZ1&g$l4=U10GqtF)idI#RcpLQ^rns zN>j{iD5f&>I1&ta=?Nr|tO@jcK;$@m2l*i+r;tGQC(s`N3F7s&AYNZ{0uBeS^J5nN zkzK$})3Z4848vC6Hma*q=@+HyD$RljYG%FXpz6S>TKoYr~uT2IUWN`C-P247@ z_SK~mZRu=7I$I~BTOyJ?kh*ksOKeJ`z_z5(HtBjBWOUJjMDh$sJ(bzI*-Xj6Zc9j8lH4X;Z-dAiYhGKR)?&k*o+eJ} zv(G%Xp6Z&+X@^PZIk03GKzdgVS%5dV9TAmK4uCHwDkLZ70GP7G2wXrE;5LLqm?0KK zC5E35`yGxK!tZ`42mxR-A<5^rF9%*mj0j4iKqf>b`6x3HnsmW4Fvh!xfJNK~QVBnS z_h&y?(oYMeY{&r!rAO&4M8-r`i_T-f#-6~c-c+e-4~76JbuRP zDR{piBMe~it3sSv=l32owErvXB)~eUM^}C8DE;#u6hG+atNG=g>lygM0y{hKqGH~= zwBbKXuA>IucwqE_Hr_uPu50>_8s%H(NJ-C!M@q)I{>p%RU6WLfDaQ***W1!-cw6Fg z@zMp57+l~Wc+>zq2Iu6EE&ha_2kRsEK1IXHA)1GRj=HV4oPJ`9H|ZF5u-9p|?S%$N3%>Rc&go#P6jPnPPRLQW0$ z2wsyNbK9J=l3y+vB}k~9v^~qy@+_p~S*=njmloK?Le|gD*(C=HX`Ca6CzYk*EOKBz z`-8xgb$lED@v>Q5DeE`rJ&?qZ&%*EbA7N1x|0)Q-7XB?n#LzC1ulpn7c_bJA)t?aW IiENf{0CoUE(*OVf literal 0 HcmV?d00001 diff --git a/tests/features/dsa/test_algorithms.py b/tests/features/dsa/test_algorithms.py new file mode 100644 index 0000000..b1c9c40 --- /dev/null +++ b/tests/features/dsa/test_algorithms.py @@ -0,0 +1,34 @@ +from mini_projects.features.dsa.application.algorithms import ( + Trie, + UnionFind, + dijkstra, + longest_increasing_subsequence, +) + + +def test_trie_insert_search_and_prefix() -> None: + trie = Trie() + trie.insert("python") + assert trie.search("python") + assert not trie.search("pyth") + assert trie.starts_with("pyth") + + +def test_union_find_connectivity() -> None: + union_find = UnionFind(5) + union_find.union(0, 1) + union_find.union(1, 2) + assert union_find.connected(0, 2) + assert not union_find.connected(0, 4) + + +def test_dijkstra_and_lis() -> None: + graph = { + "A": [("B", 1), ("C", 4)], + "B": [("C", 2)], + "C": [("D", 1)], + "D": [], + } + distances = dijkstra(graph, "A") + assert distances["D"] == 4 + assert longest_increasing_subsequence([10, 9, 2, 5, 3, 7, 101, 18]) == 4 diff --git a/tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc b/tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5944b5498cb8c643879b9646d50d21912e2341c GIT binary patch literal 6300 zcmeHLO>7%Q6yCMhzncWgZvd5=&`=u`JI-I51P9X6pQNa?aV#oDu-e#@WWnCGW;TQr zA@#taoH$hq5{Dj}8{!nifl3tzZZTFkV4Pl5N9d(U$g>07LM^1Rfc{fvlWnmezyhu#Kj7sJ_wLTz4n`O#Gr)D3l!jffX z-sZx2SHN{)nFXTT%apK;Z4_<2Xj>%XE|`(7t~!S}k3Wj08=9?uNSL-_n5JPa^3O@L zU`7b;otr=|OZPes{M3E2)^Y0c$$PP<)=peMbwjSjj$b~0SMB^_;`513rT5CjTJM&c z0Pfl(CpESAj^bTgO7Etc@I{xo4V?!3x>{Yrh6%sHWo{|GYxVI!SxBJl4-L~?n{4R_ z;?GL&4!nj`L?Q|GWDIiU!6zzu^r>E&enTI3yL#7#(3Mm=7 zO|-Jb7)1!fryjt827xr~NVJ9T$Qy9#?+voG1#*GE0dj+lK+Zbe7VMp$!G&rm)pzYQ-lKD-k(_}Ankg&07==*2S)P%t*<1HhTK(C#59 zSf8amO!N1wu#Q8)dN2I;Ed}1r?*%(O*6=3nwS6cBuyMcb_d(sB?Du=|lWMoOt?yrT z-|hEd@}BJXdvPecoBbgHCjY1F9~SoS`ltEuc~{M-7!yEyoWt}u2kmhWw5xLn;dp5n z3$a~Gf!&~rvBc8@?Eu(K1@0K!A-KZ;;6L^NynEWVf$Xh^G=ALr`Rph}XRF)1A3vaR z$ED;;KkhF)&yEOKkDQ0=M@1blUe(dIP(M2)G?DXEvwoz2oT%inLfJAgN`UYHE+-7I z6Dqiy;ex$lrQ)<3*Wo!N5JyWC!dwXhgri7Cxf9}%!2qX2oQ_;-3LY@#z~JFns-qq} z&;$+?DxwYaB^^PuH~`;jR^b2?Xl~(@F$Fx{6^M48T3$c6*7(a`X&z zM_rZ&}sYh+734D6lcSS>jQ z%=hEpj>BVP{JR`#B*!*#TgfTTPSw<@JD|$1E$n{S7hUGIGIeFdudCH1Xn@K57MHoD zOsx^W4zz{DwOhvq?d#!2Z6Tp{a?Ee>$KDwB>tJjlf%e9@+W@byk@MNjWc}s*j=;dY z4}%xn!H>cTSOn=&I00)g(;kTVAgLio)Lyd=NB+Hhlk2YPK?L_o?YtLn@WTlwj3|G( zLNS-Le-Xt1Kla?A@!+dc0y!E6N1aZKOIlTjV{c`Nm>Pwo1!RD%6-Lj31?WTAO(4xD zAWvJIx$#mh_SXL=ZNbl(RKN*S0@)n%T&l~7io6zgU(R!yn+Z$19tg5iX-Zb8VMA8S zuW}+@9d~|C27L#tr<+Pji&RnCPZg#0YPD<>d9o|58*T!wZ`WRsRMZVK&9i;3^Tjk0 z5Bs=p;-6>Tjr?5K7Ja>9m6pq7g3f{_<}4Yy;y;42EdMG=pGmi+)4xb>+z$ukSMGOk OqR7+opIyN6wfPHaR~fPZ literal 0 HcmV?d00001 diff --git a/tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc b/tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ec531c2b82c18dc9962af8c82a7fbcfa836364a GIT binary patch literal 6388 zcmeHLO>7&-6`uVe#pO~`T*tAU+SNvmWv^wEqV;3Pjw~yU6UQ-R$7>a8bXcs&k<5hL zWoMRg<-$OK_E5J6qd*JzC=|KymUC}C^pKlm#a6H|kVBAzZUN*Xh)#KL_J^b`{}m`& zw_cEM-kW(d^Y-oRe&5VScXyV+b?+bRl`j-RzCk0t?kUa_Cd>>G_dT-8#% z%hRdz#G5}Dd6$16WEMsbuS@AzYnA=PQm4}+l9ie{Ga`>hzE*lR6Z>2e35~6=>?d)I zC!wPJ(~enkoxW)G!<~#q_ZP?JywcID=kdKRNDx!&PPX=z;yIq^DL=KR^3_6c+Tju2HG_6_*F;ss|oC*k$u8UZYwTuJ%IJn{%qR>(pqV+1#0@ zI6}}fDZ;3D?it2B7HqfYJyvskoBq09bzH}X7E@yg0fCOwgkcxkjp24FpSMXtB8gj)8w6Q>4$}6?PMdp1(OFl1Dsl(hOkew@@TR1O_ zi=lz};)QwY_zgyR!LO8jr{sIIAQCB_Qog=R~^YDktC*!JO>yEgw-0}{*zARNRb zj@b7pxtH7a$9F$H^OgS6&50Wm%j%(9MHD3vzn{2OT-EzQTPlY_KeVPszuW4eWxYQ! zL*)*18cpg}S{61;gbk}WMN2IuYBC&nER~Z{+7%cv33Lzx9mGHflRyWTD3~Dh!8JAd z-Bt(1$gLV^x&xgc(5P;uWnlvZiVdqUIs_W45NM<(!-2;V4XYoJFzE*5m~72GUIt8U z=N<(hMIT6({D$-x_Mrg#P#;2!)W3unX~90aI@m|%zmR=oo`?g_AN$Y}_K~&pe>?kl z8CPNF1x!};{xq$fI7^7vA2Y-9-#VD#)~q!*Edy)qp4r{TTL0JAjTUn}t0%7rrkJ&Q zt(T^=5pUF=fj8>$SaI3njdc_7ecKe^QwkoJ5P@Vc6OBg+-~g!-tycLuvI5cPGQ%gw za3djrj^RdvA+u#jWt-BbJU$MrVeBy!q10fEy1sq2;<}UpCm0VI#i+@f3e#~c+-@KP z7~Sk?!~SCc{ZM(VsSDI+xcJx(6tL|eY$u9cD0ZWG6$R{K2-^c9=;@rcnG56Jiw-|S z(TBo7@fwPL6#G!T4x$~7M?xcnsFx7C4Vy)Q9C<&HXl@EEzQv@B>y zyjO9@m}R0r4rNZAYMgCvmn^ZLvi?-$UA>Fc=blFU}bnF z_waWQ!5BBd8-E4SPWj`bJeSNOc8lv$a?}>r;33{@Ld7iHBFHi^@Q6}t`NW!~SZaJQ zDb7>jOtob%kk`oJ`Qr8$V9Jg#7N0I@X z=+c#SoKldCW-8p(r^B6mkY-MWQyyf0;Zz$`Y`z8dHzk4L9Ofbs((Zp?i~5OEt&|<8 zjJBsXbKC{W&O#4l3LKwPvPr$C_1@h3hrNHkwrrkQ)lPnu>Au-}qxY6{Yxv7d-}eg1 zzPYBIysMqu+(44@+4f1I{GDVcd%8NX=sA2QJI+(zr_87|xNpo+W5)29aef<6RK#>F#@Ex~pPDb{;w)yW;pRY?bKKtJ>tliGE93(%_}#m^c1@c?zlKP!t{5F iBuQTr@;ma|?r+J&{j?$-zMoU11Bo#H38o?5@*e^F= None: + board = Board(cells=["X", "X", " ", "O", "O", " ", " ", " ", " "]) + ai = TicTacToeAI(letter="X", difficulty="hard", random_seed=1) + assert ai.choose_move(board) == 2 + + +def test_hard_ai_blocks_immediate_loss() -> None: + board = Board(cells=["O", "O", " ", "X", " ", " ", " ", " ", "X"]) + ai = TicTacToeAI(letter="X", difficulty="hard", random_seed=1) + assert ai.choose_move(board) == 2 + + +def test_easy_ai_is_deterministic_with_seed() -> None: + board = Board(cells=["X", " ", " ", " ", " ", " ", " ", " ", "O"]) + ai_one = TicTacToeAI(letter="X", difficulty="easy", random_seed=7) + ai_two = TicTacToeAI(letter="X", difficulty="easy", random_seed=7) + assert ai_one.choose_move(board) == ai_two.choose_move(board.clone()) + + +def test_medium_ai_prefers_search_path_when_rng_allows() -> None: + board = Board(cells=["X", "X", " ", "O", " ", " ", " ", " ", "O"]) + ai = TicTacToeAI(letter="X", difficulty="medium", random_seed=0) + assert ai.choose_move(board) == 2 + diff --git a/tests/features/tictactoe/test_board.py b/tests/features/tictactoe/test_board.py new file mode 100644 index 0000000..75e6644 --- /dev/null +++ b/tests/features/tictactoe/test_board.py @@ -0,0 +1,34 @@ +import pytest + +from mini_projects.features.tictactoe.domain.board import Board + + +def test_make_move_rejects_taken_square() -> None: + board = Board() + assert board.make_move(0, "X") is True + assert board.make_move(0, "O") is False + + +def test_winner_detects_row() -> None: + board = Board(cells=["X", "X", " ", " ", " ", " ", " ", " ", " "]) + assert board.make_move(2, "X") is True + assert board.current_winner == "X" + + +def test_winner_detects_diagonal() -> None: + board = Board(cells=["X", " ", " ", " ", "X", " ", " ", " ", " "]) + assert board.make_move(8, "X") is True + assert board.current_winner == "X" + + +def test_make_move_validates_square_range() -> None: + board = Board() + with pytest.raises(ValueError, match="between 0 and 8"): + board.make_move(9, "X") + + +def test_make_move_validates_letter() -> None: + board = Board() + with pytest.raises(ValueError, match="Letter must be X or O"): + board.make_move(0, "Z") + diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..79aaebd --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,47 @@ +from mini_projects.cli import build_parser, main + + +def test_parser_accepts_tictactoe_command() -> None: + parser = build_parser() + args = parser.parse_args(["tictactoe", "--difficulty", "medium", "--seed", "10"]) + assert args.command == "tictactoe" + assert args.difficulty == "medium" + assert args.seed == 10 + + +def test_main_without_args_prints_help(capsys) -> None: + exit_code = main([]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "Beginner Python mini-projects collection." in captured.out + + +def test_curriculum_demo_command(capsys) -> None: + exit_code = main(["curriculum-demo"]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "Capstone KPIs:" in captured.out + + +def test_world_bank_sync_command(monkeypatch, capsys) -> None: + def fake_sync(**kwargs): + _ = kwargs + return {"kpis": {"observation_count": 1.0}, "freshness": {"is_stale": False}} + + monkeypatch.setattr("mini_projects.cli.get_or_refresh_world_bank_dashboard", fake_sync) + exit_code = main(["world-bank-sync", "--countries", "US", "--indicators", "SP.POP.TOTL"]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "World Bank KPIs:" in captured.out + + +def test_world_bank_status_command(monkeypatch, capsys) -> None: + monkeypatch.setattr( + "mini_projects.cli.get_world_bank_freshness", + lambda: {"has_cache": True, "is_stale": False}, + ) + exit_code = main(["world-bank-status"]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "World Bank status:" in captured.out + diff --git a/transform_empty.py b/transform_empty.py index ff736f4..5d4e12b 100644 --- a/transform_empty.py +++ b/transform_empty.py @@ -4,17 +4,39 @@ def brighten(image, factor): # when we brighten, we just want to make each channel higher by some amount # factor is a value > 0, how much you want to brighten the image by (< 1 = darken, > 1 = brighten) - pass + x_pixels, y_pixels, num_channels = image.array.shape + new_im = Image(x_pixels=x_pixels, y_pixels=y_pixels, num_channels=num_channels) + new_im.array = image.array * factor + return new_im def adjust_contrast(image, factor, mid): # adjust the contrast by increasing the difference from the user-defined midpoint by factor amount - pass + x_pixels, y_pixels, num_channels = image.array.shape + new_im = Image(x_pixels=x_pixels, y_pixels=y_pixels, num_channels=num_channels) + for x in range(x_pixels): + for y in range(y_pixels): + for c in range(num_channels): + new_im.array[x, y, c] = (image.array[x, y, c] - mid) * factor + mid + return new_im def blur(image, kernel_size): # kernel size is the number of pixels to take into account when applying the blur # (ie kernel_size = 3 would be neighbors to the left/right, top/bottom, and diagonals) # kernel size should always be an *odd* number - pass + x_pixels, y_pixels, num_channels = image.array.shape + new_im = Image(x_pixels=x_pixels, y_pixels=y_pixels, num_channels=num_channels) + neighbor_range = kernel_size // 2 + for x in range(x_pixels): + for y in range(y_pixels): + for c in range(num_channels): + total = 0 + count = 0 + for x_i in range(max(0, x - neighbor_range), min(new_im.x_pixels - 1, x + neighbor_range) + 1): + for y_i in range(max(0, y - neighbor_range), min(new_im.y_pixels - 1, y + neighbor_range) + 1): + total += image.array[x_i, y_i, c] + count += 1 + new_im.array[x, y, c] = total / count + return new_im def apply_kernel(image, kernel): # the kernel should be a 2D array that represents the kernel we'll use! @@ -23,12 +45,31 @@ def apply_kernel(image, kernel): # [1 0 -1] # [2 0 -2] # [1 0 -1] - pass + x_pixels, y_pixels, num_channels = image.array.shape + new_im = Image(x_pixels=x_pixels, y_pixels=y_pixels, num_channels=num_channels) + neighbor_range = kernel.shape[0] // 2 + for x in range(x_pixels): + for y in range(y_pixels): + for c in range(num_channels): + total = 0 + for x_i in range(max(0, x - neighbor_range), min(new_im.x_pixels - 1, x + neighbor_range) + 1): + for y_i in range(max(0, y - neighbor_range), min(new_im.y_pixels - 1, y + neighbor_range) + 1): + x_k = x_i + neighbor_range - x + y_k = y_i + neighbor_range - y + total += image.array[x_i, y_i, c] * kernel[x_k, y_k] + new_im.array[x, y, c] = total + return new_im def combine_images(image1, image2): # let's combine two images using the squared sum of squares: value = sqrt(value_1**2, value_2**2) # size of image1 and image2 MUST be the same - pass + x_pixels, y_pixels, num_channels = image1.array.shape + new_im = Image(x_pixels=x_pixels, y_pixels=y_pixels, num_channels=num_channels) + for x in range(x_pixels): + for y in range(y_pixels): + for c in range(num_channels): + new_im.array[x, y, c] = (image1.array[x, y, c] ** 2 + image2.array[x, y, c] ** 2) ** 0.5 + return new_im if __name__ == '__main__': lake = Image(filename='lake.png') From 471a66fb3907c6ef529954ed81179da200a17c66 Mon Sep 17 00:00:00 2001 From: Eugene Sim Date: Thu, 23 Apr 2026 16:30:15 +0800 Subject: [PATCH 2/2] Remove generated Python cache artifacts from tracking. Add a project .gitignore and untrack pycache/pyc files so the repository stays clean and reviewable. --- .gitignore | 4 ++++ .../__pycache__/__init__.cpython-313.pyc | Bin 224 -> 0 bytes .../__pycache__/cli.cpython-313.pyc | Bin 4226 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 236 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 257 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 277 -> 0 bytes .../__pycache__/patterns.cpython-313.pyc | Bin 3495 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 258 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 279 -> 0 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 3634 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 242 -> 0 bytes .../domain/__pycache__/__init__.cpython-313.pyc | Bin 258 -> 0 bytes .../domain/__pycache__/models.cpython-313.pyc | Bin 667 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 276 -> 0 bytes .../__pycache__/repository.cpython-313.pyc | Bin 2625 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 275 -> 0 bytes .../presentation/__pycache__/app.cpython-313.pyc | Bin 4994 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 245 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 264 -> 0 bytes .../chunked_processing.cpython-313.pyc | Bin 1180 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 247 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 264 -> 0 bytes .../__pycache__/pipeline.cpython-313.pyc | Bin 5319 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 283 -> 0 bytes .../__pycache__/cache_store.cpython-313.pyc | Bin 2054 -> 0 bytes .../world_bank_client.cpython-313.pyc | Bin 6572 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 247 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 266 -> 0 bytes .../__pycache__/dashboard.cpython-313.pyc | Bin 1378 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 243 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 264 -> 0 bytes .../__pycache__/analysis.cpython-313.pyc | Bin 5058 -> 0 bytes .../dsa/__pycache__/__init__.cpython-313.pyc | Bin 253 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 254 -> 0 bytes .../__pycache__/algorithms.cpython-313.pyc | Bin 5822 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 238 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 259 -> 0 bytes .../application/__pycache__/ai.cpython-313.pyc | Bin 5108 -> 0 bytes .../domain/__pycache__/__init__.cpython-313.pyc | Bin 247 -> 0 bytes .../domain/__pycache__/board.cpython-313.pyc | Bin 4356 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 258 -> 0 bytes .../presentation/__pycache__/cli.cpython-313.pyc | Bin 4463 -> 0 bytes .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 693 -> 0 bytes .../test_cli.cpython-313-pytest-9.0.2.pyc | Bin 9033 -> 0 bytes .../test_patterns.cpython-313-pytest-9.0.2.pyc | Bin 4480 -> 0 bytes .../test_pipeline.cpython-313-pytest-9.0.2.pyc | Bin 4678 -> 0 bytes .../test_app.cpython-313-pytest-9.0.2.pyc | Bin 9879 -> 0 bytes ...unked_processing.cpython-313-pytest-9.0.2.pyc | Bin 3934 -> 0 bytes ...apstone_pipeline.cpython-313-pytest-9.0.2.pyc | Bin 2573 -> 0 bytes ...orld_bank_client.cpython-313-pytest-9.0.2.pyc | Bin 5667 -> 0 bytes ...rld_bank_refresh.cpython-313-pytest-9.0.2.pyc | Bin 5045 -> 0 bytes .../test_analysis.cpython-313-pytest-9.0.2.pyc | Bin 4225 -> 0 bytes .../test_algorithms.cpython-313-pytest-9.0.2.pyc | Bin 6252 -> 0 bytes .../test_ai.cpython-313-pytest-9.0.2.pyc | Bin 6300 -> 0 bytes .../test_board.cpython-313-pytest-9.0.2.pyc | Bin 6388 -> 0 bytes 55 files changed, 4 insertions(+) create mode 100644 .gitignore delete mode 100644 src/mini_projects/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/__pycache__/cli.cpython-313.pyc delete mode 100644 src/mini_projects/features/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/advanced_python/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/advanced_python/application/__pycache__/patterns.cpython-313.pyc delete mode 100644 src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/async_concurrency/application/__pycache__/pipeline.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/presentation/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc delete mode 100644 src/mini_projects/features/big_data/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/big_data/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/big_data/application/__pycache__/chunked_processing.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/application/__pycache__/pipeline.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc delete mode 100644 src/mini_projects/features/capstone/infrastructure/__pycache__/world_bank_client.cpython-313.pyc delete mode 100644 src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc delete mode 100644 src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc delete mode 100644 src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/application/__pycache__/ai.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/mini_projects/features/tictactoe/presentation/__pycache__/cli.cpython-313.pyc delete mode 100644 tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_cli.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/backend_api/__pycache__/test_app.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/big_data/__pycache__/test_chunked_processing.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/capstone/__pycache__/test_capstone_pipeline.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/capstone/__pycache__/test_world_bank_client.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc delete mode 100644 tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93c42f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ diff --git a/src/mini_projects/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 3808b35db63e2cdac1d735964d33ab5ece4f9aaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224 zcmey&%ge<81T&7l%v1-`k3k$5V1zP0a{w7r8G;##7}6OvnX2TSQqwc@@=}Wwax?QX zbqk8}vr>~wiWLeHld}`kQ}z5b8E>)2$EV~c$H%W^_zW`TR-k@pacWVqerjoYYF=t_ zX0Cn!RGWTIYGP5IZb4;9Mt+{Y5111G;S?7o>w~O_hgqQ?9}nb~#K-FuRNmsS$<0qG d%}KQ@VguR$a%?fkp&ytT85wUes1~sRIRGSHK`Q_N diff --git a/src/mini_projects/__pycache__/cli.cpython-313.pyc b/src/mini_projects/__pycache__/cli.cpython-313.pyc deleted file mode 100644 index 3a74bb10942e1946d704fa443b2700e939146b1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4226 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 diff --git a/src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/advanced_python/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1653266d621ccdc97f69ff2d0cd46b3c8b59bd6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277 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 diff --git a/src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/async_concurrency/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ecd33e47b4594f33084b3e360d2f0dd37c364f78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258 zcmYjML2AP=5R{VAgh2kFQy>sW9|(coO9+IV7b9<66?-J-lu)iZ2N8Uj{58y+b1dCj*C)d@BZ^@{XGG!c@h8fAU zs=$=VQm#suH3e=DYuU{JGG_ojMIX(&LZ3ad*VyDp=xwQw?B3DTUjLsbRa?I~(y!4g zIS$0L(WNB|)u_E-S~rYdr!fgUq|g9CSp#gVwwv#UD0_Ym&tn}QtNc!-l%3e{Qy_mH CVoxyu diff --git a/src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/async_concurrency/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index d477e724245de0282c108cee51c5e577e8c5101e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 279 zcmYjMK}rKb5S-DVLC7EGnuF<6Jc;04Bp{xfVYZWu?Dh;jvy$~6KEk&MK47n&yb1dO zXQLtZrK_N;t7@3dE>xNKk1u~!_UA|rqyNA#E#Or>C{Y_dKY4-M7;Eor_JK_zdG?Ny zSqCvTb?l7g%DBL;6(M%r;(o6CbHpk*#5XmJU;7%Yu4#N_I%dsW7vV&0gG%~2)h zi@7=FnH56*N6f4cvAvpMdLd3~661I_ zbIAV&xh+OzADDs)ec;8_;$l(&=m4F>MV%-R4Fq9Ya#jRzgEoNrTQ)Pgo)5Wf zcS^UCb2_yZs++k2P3pG0s^4zNXr`6Wxh|5hWlouFlPZ7gi{ZGbQ!}g=W_3e1)7kJO z&0WxwmKo0I8a4Lh=dHP%5&j16OgbHtCd1jZkxt}myeVC?3b=fjTbD@YjAVgQ-AK-d zwR}F4PV!(0=hJyTlQ#6h{Jh=7%}%_Tw&oJd+;u#54;L{!G=ww>APS8R|)rm11(7#HC>&CSn3B z7o@DXM!Fv6>!ihD!XZP6Lo4j7PL06E#Oeb~A&CdHD@vKiE_JW*cp{U|rmc0MovbD` zSIGo!F);Wg5@Uk1_tOG_aZLHO39Q0tf_s8>DYq`c5d`tKuX-xu2#V|FxurYM+75g@ zK^OqegJ-vzw!67e05fP~3L&Eh6Iq4XHEH_+iGBbfkv1YH^5lp(OaVrTKlW?ozWYkUzx{H;;K8+M1Dy2x_%@gaosl}zDr(m{Zt$gf9}J9 zUz1_iZ(K^ofA^4C*(QAaOPs3j@f;2t}y+h^Rp^wI&n#}gA?VX6Od{J98Eo!?EQVP_dDo)}A z19OU%23;4cxz2XYY8K)qLawKVoTlU7=I6qhvaL{Nh{=kX`YrXy5y$7v&uxP;qdDto zB%alftm~Z@7u;Jpl#MPl9w;jZmYX^jn}YY7g1D1~rs3lF7m^^qPyuW{R{)`@`)2n) zg3;3WLNHoxo>=rwl%ErhWQ!vC7gSSZzZ$cDrJ8mnT)pG?ad&o1|Fnbe`6>#BM&eE2$YtJ!8AD z*y+X$i$1fEu|gh-`m2}`sH#brI!4!w=>)6H!NHbhat4-dCY&~AZHbMtD$cZ+CflG> zud#Kat~I3@Xp>VutC}fG-Vna#6>M18J#K(wBuW;1JZ*mb%|*X@-><&c@qXt*@THIa zL#6TJv9d3^ygB%bfp-V)7Vd36zSwsBd#8%8eA3i$OXPa0~ z_CW~C5l`|!7SAn>x09r9n(RQgSaaw!cD9KeO;oQ?iMjdnG(CfWrKEEXTHBsvzECCQ zZ2F=;b7`3QB4N$v^?FG|U#Yp&0ktMIpw^`ZR4x@F9CFy*xGrwHv1a7a1d}CN&5@nS zxm?cF*Itk!6rmk#F>P2B zvm6`{Rd_5v^JLPd#Ra%ldW13TU`>QIoc}0NxGIl*Mz7%<#I7x;GC8fPoAXNaAY)Tm zv$|q~-H=G63hZK;NKm$ws&|vQ4E}?`|NR)$&L^qFrdSB@DIhZy}?P(c{=9_?kb%rXmtT{t3OGLF1%v1)_k3k$5V1zP0a{w7r8G;##7}6OvnW|)*5|gu2^HLNX13VRS zQWJ~vGV{_Ea`RJ4b5iyEG#PKP$H%ASC&$OHWcUm+;#Qh|XmM&$v3_c4dTL&3ab~W5 zKv8~HYH~@jK3KbML1jrsexAM$$SmCe2&cFxSwA;3FEhRXrZz1#v81#pwOBt1Vt;&M tL8g9uJWx$Ze7s&kxb`Uhr4M#ftVN<}O{4ggy)N8|ti diff --git a/src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/domain/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ccd35469ab4428fc4aa2267892464e5c9cc31f7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258 zcmey&%ge<81W(Sr%+vwWk3k$5V1zP0a{w7r8G;##7}6OvnW|J>@^ce2^AvLPQ&Mw^ z71HvH6p|8?vs3d@6dVIQLBgdusd|2zjJMe1<5TjJCpEDsPq(15BqKjh-v`VIfN+Y7lJ#>l^D^TLU~1D+6H7{q zQj7JW*2X6mWa_7Y9i<;14^&zbAFo$Xd5gma;ugCiHlXbw?-YZ)@`0I=k?|ITb`cAZ F0|0o$P2&In diff --git a/src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc b/src/mini_projects/features/backend_api/domain/__pycache__/models.cpython-313.pyc deleted file mode 100644 index 476add176229f917f3782ffce5955f2438d6c6f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 667 zcmYjOzi$&U6t>TINt%!V5-mTbQVF3O;1)U{AqKXlRaEIPB6W*gY6qKSAvzL z1AhYlOeMC;QY9qBkge4XtUTW}6_)(o_xXL#e$THS4*Lk!`T1IW2mjlX&5Bxp>vsUo zP>ND~h{jmsU5|sqU`#aGMN_heQj&(R@goE%dV$ z6-pYQTRF2VE0{6wfkmcJ)?_z{jUz3=^(z2pXo@sWkq%O<$ulTq8m8nqMA8sYbc;F- z>gs6Gi?>`jQI@YeAag`W>$*?@ry_U6SnVP!Y{6YPQ~Bx#n#3K~qg1kzQ|kJZmMX6b zkMB_Wxnjjl_YqgkFmK#d-2+F8oBX^Q~FkG92 zR|_t=5oL0$)hC`=lN6k3`C_@UAC*jw8gkqcqqC$GQqbi_JLk++nww8`HFrG@0}i93`VEpi~jJ-f!^L|CUN9jpI823sM!y} z4Q@xpjSe@`rk^%*$E$j8+7ElH9|Og(O$_FWV2m%(-XHYzx`Tqjw+BD*&fkFG(G>zy M6BInW_O!X`A0v*jaR2}S diff --git a/src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/backend_api/infrastructure/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index fc24681666bcfee138192af8bd74348e5665019a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 276 zcmY*UF-`+95VQe7gp@zHO+mJTDng=+gwRQddS!fj!6au}Yr6>l;1PU=2V6^m=#cIM z*d!@3&CW&&)TfXwd$@}s@}JGL-U8394-)BZxBDkSg#Hlo?@{8-H z*rpfn;Qn7UI}zDHZ=J<(*zHKy2od&gQ3nh5C>|gdOG2E7?J!yFGTridNl^>NTJHB$ LN;!yYJ}cr6X%AKI diff --git a/src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc b/src/mini_projects/features/backend_api/infrastructure/__pycache__/repository.cpython-313.pyc deleted file mode 100644 index a173f85f085eec29c6f76252fdf2403b9a7ec043..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2625 zcmb7GO>Ep$5Pr7z$8oaV{3uGAw27NmRklsD;U|Lrh_Ve^n!in0?^cNdvK+^oxY@PS z_q+;;klGVU1ybQqCIhGrziWR96ta713sJGBWRNR=cchlWet<-mzH}B2N z8^8Hx=6O33kqC_SyARF&AR!NMQ5gZB;kI7~<_1xS!i|ylI zP9flgqkE(I=eG@wX|fIJN<_2?3~kkX(8Jua+-s5l>vMcjSpA#E(#%yewJ z@=%0h_42oQuHeQ5oO0Wrc@Ag1O(f}Rt@3UU$#M#(@F)2!$?wXmY(l%{yG)j(%Te6j z0w%w^2lD!AiO&y~w`4L;VnXUX1glA3F!V*0X$3=d7O0`k#X@eFUCx`1@roPPZObxr z$Cb(q#Ml+m_Oe?~?PWEm>3Dy4pK4gFNDY<2X$39Dy8&k8b8ci#bF^8F8S0YeEHJ#A zEW69`EHfyJ8^w9UGMHJ2Pg465cwzCpp;7C^(u%WSTk$b(G3hTD)#C-zGS#JuZqCpg z@X6w{n!adQbE>vv#!V|nHRjNwj&WW9_T?(`TZtRAQ_YJ-17;Z?n3kB1O zakLTk8AeQ|%|K}s^$zX@xF$jOp~-O|R|$ZKaBhg*C9!*p4@DYx1hG!qY9@6NU(i(& zyY7iHZ3TV38ZRtM58>~Dc`<~inqTEB&CC*>ksHrNc_s`_y)lDH>y50C_L^>G?a!5L znrem?BOJlPU+o)CDamvO!JY6kpw|PfPexc`^2NkhHmUTU?UT>OPTv;Xa5)>?pr_^r zJhhOA32g&Ev>geyA6GCf$F0j_723<@TrA)rL=U1u7b=AGB4svZsEwKC!RFTM(0m}} z(39J-2fiv<2UxKSKmfa@*3Y{??cQkWEj9ISHk{gwG_Q?s2Zd&7Ewm-p0Jtv7_oFRe z4Sf^ZkOxZgz;6d$`l+=P9r;xp@qw)_T@0uP>@8qk4A@Gr)6W6V;<8)|xyUma8`mRg-L#VKOyx*21|=S0@UYnBo<1oo>7ux$)QZ8cKhTi zQ04RKiScr^Po&?1ias}8QIr$P&}n)Ad^~}PcES?C!?N=rcEz0G=nGmt@8KL1JY3xX zpv+tWDHg{ro>Jaqj)x|5_wRx8iKqummf zRNEp#o%G)$0xZJ-{?L_(2B2f$mEcEVRXkNN3h;iNf@?Oxo-07kx*=-7+hO5vgk{8d zihY9i0f~kD=p4fh%w+0uo*Z+zth?&ndRs?rub9UB?N0MT9`ZmX&?{L9f*&2gwX3tf<@>TTlXuiyHxBT*#y zL&C5wKP3P1La=K`l7feJ+C#xn?r~!{DET(139mhTSFVW9B{)V1DK-&zpysvhue`SX zRj+OToL$gNs~>J<(3C3ceOZmN0~<2^itm+=6zb>&K7 zE?|#IAa7IeLH&HyPbOoP_4E1d?sEMa!cqEQ`cXFT>PfL$>*?9sO~EAOlT9volMS~q zIG@C<0-G0GZpl|>Q7lazs%A&C$EohF5UWrkeyDy}+X*}iu>dt|@+7W?g2RT&B(NjP z{yMhlHF$XV55+zRmry(JFdlnXWK)xI;BXdgbP!MY05K0F#93^+(R@#I&zG5D_)mu4 Oe$Tbmox0_V9R2_}=2fTw diff --git a/src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc b/src/mini_projects/features/backend_api/presentation/__pycache__/app.cpython-313.pyc deleted file mode 100644 index e77be4e26847ce1db89ed2ee591687dba009b92e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4994 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 diff --git a/src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/capstone/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 3c85096cca9c8f5dcd752a067dd8231d0fb3cbd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 264 zcmYLEL2AP=5R}@IhC-fD@YRQ8`+!pDy@WvNc@fISQLrVkD;JV~gTlghE3$hz2)Y)a8;oj9z+QshdaO)c58 zR9qCx~EBE;dRbpaqJ&QMKIye(IdF+9fS3 zEjj|vWzKER*_ruf^uXhBA)w!uzsvn8fY9Ga#~{1jU>>wE2)&O4BrtPmmcdMzv@LU% zSr)Ui7Hpw?cFsC$!#3Ks&T+GLY^QD8oMYCBowF|NqJ3`8Jg6ky)2cULC-6HUUM`#k71+U-}+;I-K{?OHj&K^jMgF=Vk??AV# zI3xsw7NPavFBs=~(D(fbokEDLxn;p^JxJ)%=Ly|H8|5;%{qRbmM`)*~bgZ_mgPbzUCI??C1%lBacM$xWYZ<7lv|fzoE?`pp$bU~Fo2y>GU+^&B@CU_O<3ZZ zNN3ihDY=9-_n<}O^^M!O4Cbf{dVa^IpuS;6j9dbr$84fvOl})pV zf@YZ**DRMWX_m<;%`&?X8}ZAN3-N1nOAGP2$@r_6x0uE^IBXPum5k58`1SbYEc>0qi7E4CnX8X>AYqy7G9TdX)^C)xxGK3vQdrF>47;>Ask#~X6Wn1%a6jyU|3^H6M~ zT|WFtsKUp#r*^%ke;=)Qf3Q8X+dTLi<%_eQZB?4jY+u>)wtqNS_D0m!9<}$_zTL`m zHDtAM4`4l-&Uu>`I7(pA&vmA%15e3L!EAbIy-Y+Kn2NL$Da1S7Jo#1?D(e4U~l) z!YBzVb&@%eTQt?g;KEMiBud6UvOVcAc#G8c8a=)4LrL=WDGBTtBRfR<@@t?}$UOUH z=-C&Wjal_tbO_e5x{MQWzRwti6>z)5WJ=`}Y%rtc@Hjmw_%wI1{b`=zi3Luu$E~7M za2$h4qH_R=wuYC3JHyKnYoKJTl3mGegD1K~OT!BNt$WN-_NlS3F(x?2SlOKXZ>QIpG?T#x7xT-0biNg^(#WXx!Z=1gNn!1)P3iSLNV+G=YsgYf!>QVK6i;foi-r@v%0OKA&ARxaFG zng;*jBlsX?Q!BWz4(1A$fd^p=tu>!kN-1R{Lp;&SMn1J7l`?CZBd37n077X_YH{F7 zhzmObOE7sQOmvZYw3v-n)FEN=0l_0sSC!JuauirG7F@CpJerFh0FtCjRDxJYb6rp8 zH>4?y!KF8)Ca0#y#j8uHiSdamQyRM}l{6=9r{<<^VDhE#Go&Cc505~hSpnus_*v2? zcf!vRrZcA$z-a*1Vq#?4G5uz#w46=JgrCoCNvQ_3GaRhX6kuLVEG^DV37X$v%_Jsg zCdQZM7llJ^8WZKzSVbyfV%)JNOms~br~Y0i3E7*9rFlOY4VcO9Aj7ILyX-ngE2SVdfawP?e9}zqmc62HHLtdvEOijY{Cy_SIdN|J{?n zJbAyr?CRN`*mJa$9pS%vyH#&j)f+8)qhIvzc%y&u#{cdQ-g~ne8Y+i|D*lsq?R$}^ z+Ig(nIa2N%QKP4;(evf#`D%2!9G$L2uc&R^`%YWfb=Q8+U3PTUc;pS;ZQAn&tKOme z!?#@e`=1Mz|F1Rj8gJUZTZ3?l$9=qaZKe4S)QWEKI64pWlo zI3>gR$QIF(u_Fk?1q+1WNMPgEfd-ISB-(d~08mVrA@CqWY$93~Nt|(rAZoFVF)&8d z>JTlOKvEwtTsN#Z0$Oz0Vq9W+2^^4fEv+0l9%8ik3@NC&5^slxp@_LKQAd1^6wj05 z2y3U-jW3We$c%_|duSu}&|vJQQ^T{cun0fpEhxYUIOJ(FG@kdp zJM)W~U*#%%81&!Hzx~!;dym@IrAA|FXD?`~*HuF{r|Th)+<``OXl++p+CNCVm-s}v zm)L0;QX_-a$mw$AbT#sPIr4lZazPDsRYS+iq2sU%33+TbPFEq2f)Y^&@`<2k9$W_l zeIE@Y8A8key7CD^vQk-F>|%pme+;|YMRpO? z>o7rNaD-w3*HovN2j$N~Qdg#zvo@yTMySXZwN#RL^}7^V|O z*A#@@2{ARcOBmZdhbrCe6Y%Qm-+6!uHKKYd~`+a?fdwRkKRxRj#URn%LAio zWZ?7ePrK{=@Nu;}{PB&CZm50H&*PuQt9|Fnedj8D=l6Y1pZ{O1+tc)bMJ?fd4{GnP z4I)0cV{N6@1m2++gnk^#=af>Cgb7JVnNEfa%OuUYc~j%C1bL3U<$G%E0Kzrva-oo? zu0~^X0Mi7`AA9M3u+`1*o8bhcM|wQ;8jMkQ^tk=$1m2i=0w!$*&CFd-<&D84*}bVQ z4E)#NW{gP~UM@urAan4KpfEl)F+yD~rzw{%P@0U;`LB}soti6^TG=3(T`Gl%htWMO zffa+ z1k0J*PpnSEeBu0^l8rzqMDzA=Qj`Qn<>_%R? zh;P9p;_($1lr@%Nm~TTb*g=X zYX33y)Mt{ICcY@Y?w!kAB^lK+NnBX4YS+hY6XL#FdcG7YnUq{VJS}w$O LKXOBhzSVyK#Us?b diff --git a/src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/capstone/infrastructure/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index bdceb0a86793e31f70ced27583b85c8d6ab88bd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmY+9!Ab)$5QY<5v=sUdx%SZJEJ*8BkWvuOA!KLQ$Zis5W)Br#!-w#3yzbSLH(_7E zMBPLG%ghJ=4F5l_R!fof`tjv&^ZYC@N&YY6A}?>^L7-^l`r_sKF4Sm=v3J}fC|lXY zfTZe(%GpF511JRBZYs-G`OpBdk70XQ%i+qHDmr7{#aR9is2?dGYv>yYK)%yQjL+bh zbPE>4CY|^x27Uhv$3Gx+y7R%CGVB@F2)fc9r8wK3Vx#%$ARSlo26>RZ_1WsXpK PQt9|TlTr?1kMj&aB-B|A diff --git a/src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc b/src/mini_projects/features/capstone/infrastructure/__pycache__/cache_store.cpython-313.pyc deleted file mode 100644 index deb51a34a95525c2789aab7a8518c829acf92c32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2054 zcmZuy&2JM&6rb4-du?M(h)E&>>JkF&woTlEQURi$sD~zPAYLq03e{>^kIkl=-E?M5 z<)}zKwQ#85l0&OX^oqnE(LVu`1JOvRl~514L5-Aa-`ll;q>QZJysz2$&71ee{c^cT zU|jEQhfg#@o}e-uW+u@7br3d)OI*4@<|(H(%G!c9uX8<>^#x<@`Z48r8h2LP!?F23|E;dAU0VfeF)S)jk}j&Hbs<35e)WACp}YW|1%Ia z$Q8n=OStAzuAhMOT`^q!P4Kt|NMsHT-$QWs(8Tl9fwh5;@;OJnV8yN=)7GnNZO zLxa@Q${6a|>}qG}42b9M6EaO4eGxLN$#uZ|fVKno4*^P#lkYiJ5Z{lGM>qfg*JTJ6 zwRja2Rp6|}IEw=&@HTAQR-IZyFfMAW(`GRf;cD#?PgYnUMJ-}Jk7w4p@>&wtzD$)% znId>lTMgsTTN}oELvd${7n8XPi`8MTL&gKjft(1>ZVcMF*LJIpAp1tF|OOyQiIZ?eazMFmU8p1 zh7H3F*K+k#GTnk}q*Bf`dA^-k2%toe}4= zXdQbEXIO-nDG-2eW%TxqTQ?q6>|VwGV|24};(>J{BmXLu7vybNwL#!jfO+p|P<-Y8QwZ-MU_$w6 zglU95t#J}<2v~$-1mGv~T%+cx=Q(Pg+yOYA5yzNCr-Uur>fj3lZHC!Zr5AryOmG7% z=>wU)jrc68ob0ZJahqfFWxUf(cN0<8`Rpb5h@XQvEFAGMz*C)4`j|}pLyrALW_o1i nF&TT7BUk|>Inxq+n)DPtgTNkIV@ z(A|Y(!AA; zb1typhiPZpEA#Hzd(J&~aql_5bI$dK-|r#N?v&P3nLUJjfiHFwYXrCP2@ns7N>uJL zncyfVBkj2CnBXbTX#TP=AyRQdpaOdrE<33cWW>v^35iPeF*kL0kO)zookVrbdPew~ ze(Ig&VN`;D?&A(7;QDqFQc2(={Wt7`;j} zx3#2cMAKS=>Vw()=B(Mi*0hDxS4v=e2~HqnZ&-CbEW^(Y0twpQj1K zq`4$AqW5r9;y2-!#*=JYhO+mm8@_;b_&o+1)DOs-FiL~=_;=uFobxo3*K|uX(izj( zb|iHZ;WlPr!uBH>027>rL)i};MhOi{Y*5YyaIQQ zUiGMsPI6ng%~1ikUVFUuRDCL6=ZdOd6_BP*TSjeCMKz(+m0(5pm^MF5Y<2DcvNeq1T1ejI=tZk<5&1f7Wbz0+M+;rWfY!&!ad0Qez(sk4! zXz6)+5cJbS*bPEw;Oa6oiieJn0nTZH z`*-ZvqamBqg|l9T?mbfV5ns#Fn;(p=1-pykqvham(KlS_kRJyg1)lKbjzb0EhyF@? z&kuc--l1Pg-OMny?XwH?uzmg+{A}1~Zo09%z^OMIr_lzn&EY9Dc2MQ)J%&?oe1pu^ zlj2prah97}giS4~I9-oWwe@N}T2ai=LhP@x<#j=I#e^n^Gw}8Jdv-KizKI+abTLYF zr^@TD9ugzd2x2>0OjO<1M^(=duX>L=bP1#o;dZo`WUrnBtIB>7b55g+?`SX=eRoHT zdF;9NQzkLj3_<{FF}Lc&pKo(d{4>}+>=S&kvmYc?eh}8uT~}^GX!K9zJGNqzoHAG_ zN>Obt-&KRNh8aT$MN`%mv*vw@2$^rqYBZirENDCId@Q-u4DgVSqBFh@#6BXUp^z4G zF+@ma&$k)OUGh+r$(1%jyfABljPmoG!eNLOb6Hp#LWE=JFh%b-&^v7}aF1g|dnZdz zS+DJM0|v3+Bz0(xXEq_-7#dG!l8Ll&YN(-L7|f8PX74hXRoci z_PO#$=6>Jr?&0$8XdV2cc`ro3y85Uk+%_`0!b2>}&_%#|$=)(cz>R~SAZhwsHj~m# z%Lh3Cy$>5o6N`o=BHvKl5dIJ(r~+RB>Ohj>S6TjeJ!y#J5>{s%B1sBE3yd3C$aFN& zbRUAuoaNNsF<}+Ui@IT)X6_jl4@)XuIt03C7!|r3=S@%Gd%zB7hPJQ>=?opmQQy`b zqhmPave%IoH8X+9l_lU32AV#CQ@zzNs{7lHz&iAQIWROr043_m{b=s+nl`7cU2 zGFk4NECed8JSHEpHa2=SN2hzDl6EAUtmhR@ypO z+YXf44pjE`RSpb2yI2aVRgttSRl>J76)sdI(i2$?kC(#Z&#slj=O0WJE>?UUtG?cn zuXjaQl@FKX!%stH`7eLr8?6Ld{?S-IwsN7=b*S7vSPl$Unp>+*(%fB?x|2n187Xp9DQU4L4Y+ENr2S@v`e~h{pxPJ z#lllB1Ual1Ser(S+R>o25n|#XV70xHOnRY!(xubVm7kGK`WfsO8=dFo$w|JK017&% z7y@IGT!#x+Jz>ZXFb~=+E$YtMfarvrP8sIHCy%iG(`)%4)*Mip*d>UWk(DNvW>_Lk z=nERlHZ1X$X=dNC_yx#p&={;{iX}}PaE%ojIfEXv-NzD>)C6?w}?(^>? zwJcU(mVjT1Nxi0J6?YAQ5L)O-oX=w#g2jcfE03W|(vU=X$e zrj`ZK)5s3*1d?rVc>zR>GIZ~e%AVGb!yks10xQyTzS#6ifv@a8Sn#a1cQ3!WB7Eli zvG2(paL-@09|1>pc>F_aUGn4OpB!INe{QZ`m?^=3U9(FP;1IvwM14KGW~`yx((H5kDV#$wnFrLg?v$9o)tr zfOxniHI6lsGDw{7)f49i%4|=ZRUXp~-4Wwdfl>SnK3I!6HW6Z1zueR*+Fqb>sx#&o z1N36)`lf_yQ)0^s{7eIR{>%!Eq)X$j?;CdX6U9BXj@ywR&SAO$r7yUF?Jl)=Jtj98 zGj?!0Bx7*9L2+s=4~p0<8M1Y>@l9vb<;+bis_7|QsG}jcm@>dm7Ucab%J0v2q0X;Q zXTAzm1E1FPTG1Q>Jy1fXK;>INDniYH&?V>uc9)^sUKY?ZFl4>*HHY}dgyFAI#os{p z9(gGdU!ZXBgYo4fWpDRd@Gpw{Z?7kzJoJuV;l@I*nX1lpGFJjfS>wZPux1D~E- z4Jf66!bC5ZgOf$yWW^g?K2Y{X*x2cE@J!Kn28H`S6;=ZSrN97VoGS;<7k%d|f%d}R z7Nu}4v~4w4%!Zk{N-+A6KrZf>fuUNP-YQuii;$T{FjKkXoX*EM2wwjV<|@R5-Lm~n z)7HfpbCxT63Hb(D{%|8CLNME1pRh&76?5!XGt4w9gqcPa0HF^9`HrSKRM9qa+Zq1s z9H;8~mN~Bf$2z;uaenK!?Y=v3v-h=cYt%fk1|-CI)pNYz{!kKpQ7Z{z=)_(u2pndf z%CK9k7@*xiS+1P^T|IM8w;XWi!pEjA(;gfqimN7sg)oz3*)L<(RP+Huk!n{mJZPW@ zXH><>&IHsQ`dsaD1q_2w2=ge+vS{=?7LVbkkD?Zf2d`#Ix7-cs^BJqDQ6WGnGM_OJ z!t5Z%vSfM=r+0nLWjFKp6bKk#$>5`v!Dzv`)*AkJ_M_QP)l#cc@cgEwy>PY?YXA7c zhZmMFltcXm@T<^+@BO;Dqtey$c=XZe%JC;R%Uwqbla;Xi_}ZguD>F|cI8*LByVldUGPD{Q zE=7i)UVD}*M=q9oE}`Vxs{^AY2yFrk4qPnvU0U1Qy{xkJ-g+`!4j(S>J+ju>yCVMR zP^DLS5s>x-|BLtdn_l=ysG}Mo&20rg0|JHLaq}^y@cC}cNsJTGXd;^$x`+3Sc+EJJ zp$pNwudZX{Sb#rh!>_{`9Rz~1Lu1z`=kXOY8Fq$n7o)*-C1fX4aEGaIssDuOFbON8 z+N8e%D(}XOW(Ja)8>~j6Oo;)UKXo!rk(kQMu ziQQ@JFmy0u-1U;NAGz4E8vFwwen6m9**jWk@2K<*RNWpi`aIZL72yfvK)H~TNM|qO zRXxPh^raWaWU@D0b@zzf&)fD^MRq1vdg<#G-kJ~Y!|#J7R0UvWu@`jjr}S5F1&K^XS@Mj zmFGC_wJ0%L)~N&o-= diff --git a/src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dashboard/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 4fc7484fa0e9319f08cd5ddd10c8a16a36b2772a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 247 zcmXv}K?=e!5KL55g!%@-Qw7Zj#FIx6K|C+DS!<+C%BDTkfA|Pr-)dEob#H6)N|qkv)4($ diff --git a/src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dashboard/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 60ef6ea228b08e8edd70e7c9ebee39de5c778b23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmYLEK}rKb5S)pU1R>xP_L_icy^G*oBp{xf$!sSX*_|1>XNC9=AK`1f@6}Uof&D;c zO`>(_DyZ(R>L!ybm2v&Ob9Z_E^x_PCBHcKXPxYc$t@U*9F?)=$a@Hgld?Cc|&XN?X zz(r|fvkZpIq7G$K(foO;+Yw?JEaI2y`h#tR*SsV^&6+%kt6{<6jcgLA$gsbO+q4ND zo_@vRD1Xt9F FH~=wsQD*=E diff --git a/src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc b/src/mini_projects/features/dashboard/application/__pycache__/dashboard.cpython-313.pyc deleted file mode 100644 index db628c0efe5c561735150e11079af77d278f9797..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1378 zcmZ`&&2QX96rZuZwzD5C61|PpxNzW&H=CqQc?ZvXzc=&zK7Q}8Rx2Zr z-SM+1R1tbAjkzc@1s|?}@({Vm#cQaI8E#5lSyPw_r@E%KORUt^nGU$_Y8TNxjTxYo z3eD^(w((4cvazU;BuR6UM`^;>ZADl?+Rp|#^`j(=0+Of9zn?{1)KBd0&<6+YrEC~r z8RkfII3Z(v_%A3A(Z;dQLrX_`f;ZsCOirPShdQM>esX$HpM*Oy@=MY&Ujo2%6<1wW zT&;LhIqP3Kf;zRcJ{P;C53#E+<3#nar*zK0;*Q~(%h*%@-{&s0)XT^&uL~`N7Og^q zO~Pd_=>t*vj>H2h)Q}7TH-a=C^b;-&MuU`v956j)5lwPi6w>ho$#ukaH)dgeBLrVSYDFoZE$QXJ&8~IQ=Mz z{A_-AlahSED0f1_?{-tdLWg8oEGY)mJ3(97P%L%_Q5>FP;PD$jBf~f)A-f3a$qDnH z;j@FDR2F}Ecfa!f&aEf)4<0Y=*Kh2s|6$gCto=~irH@vB>QAgIW9!Pqx;nP5?$PgS zBXi}qugB&W6SF-w+xzD2iRq3__n?01k+=KhR7DG4;VDuU%Al5Bn^w_<`a$EpNy8a8 zoW0?w;fxxe9axJK%N|?yUSnk0BkPmD46Uk9kyg?VAuII4a!A@2%kd^CuU-xy4&J`; zISKA>L3^&tMTF+SWSUzHdPP}tGi6QsAmVwH^qK*QV;D@!#zi;aupl&-o7Q9v_=-1X zeLUN}x46`{7PS=hQoa&4C=S?=xAd&@+<2kA;$j;^P;n(3(@B1uH^)oKV_G}B@5@Ws;P=G{vEyZ6uthBrefuJwTkt>-&XKP H#oPY?R1seL diff --git a/src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/data_stack/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 16ae86885bec4dd0aa9cb2f27d378c01dc80fab5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243 zcmXwzJqp4=5QTSDRD@hXQfOhJdjYYs6bXp+uq0z#O*Ua?Q^b3C2v1|5*2+%E1>E&# znt2c2y!qN77%?}a`%_Kbet7dA{uAxci7VSN!%{x$T*hl@Whk}GYEl$tD2-M+4f7(Y zGF)tDycqx_MGSCbtuJX%>yMVgVLq-3*sshL!2Nub!Vg`>V@2Z`j22oOuISoJbu2 diff --git a/src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/data_stack/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ad48727d091c1144f3e81655484d15c534cbe867..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 264 zcmYLEK}rKb5S-DVLChcaCOKsL10r~r5D?GJFx!cvyE9|=Y+&;bAK`1f@70qxVL#xE z8mvoKL3MZ4v{+oIjLY{=SLgXr#1Zr!6Lq?sazXh;301aP`@Kj;yT!Hcp{qwS~BccaY&EB!`+YA z{0d4CX%-AZB|)tm81wue;FnE#2^0PEbloDfD%!wI0=q HXIXp$Zn994 diff --git a/src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc b/src/mini_projects/features/data_stack/application/__pycache__/analysis.cpython-313.pyc deleted file mode 100644 index d65b69169ef887b7b2ca6b1d31ce7cb3738b841f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5058 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 b%Zv6fkV8(|@gigg((I-k|A;tfBZmG9gKHFM diff --git a/src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dsa/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 7fcef85d841fa3459cd76c59602cbbfb66a58c85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmXv}F>V4u4BRDAK#};vwiMthRER2N6d^^lSgSkdSai30viAz|k31ru$P@B_YpLiW z-3PJ@VH(fK_KZDDr;jS(`{(SIsh+a9LI0CslE|leQ>-dI8-2jDjn+uyu819z80$-8 z>ngB|`$kMnmc8?pX+qi6w0NEAevDX#0`W@?*|p!nR)__ttH_hM1~wcHR78Qx`n)~H zeem#-z2=*%5H6r`-eG&2eJ6W40ZOren0gU$(H{HB>KeZ0hZL7Tk^IH)oz}WnPkf)m EA93DH(f|Me diff --git a/src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/dsa/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 2fc003dce37c096544bfd953d3f1ee0f8adf39d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 254 zcmXw!L23d)5JkIVFcR<%vv6Z1)&+tfWEn&d*F`f^jx96YO;ry>?~y~~7M>snm~B?s z1?B?j(O8@M4?gwlcjNJ+N^}1Dv9q+ld$^7M7u_g{54BOON+0JwJ zx>;uQ9*h&~T~_;|sc62M>h=M#@HyhA>iSdd0k%RcKut-G#MZE8f1o@HRAksahhyA( z2X7ZxUqLvB+B%Ej=G~Er4U<42222Q*%}q?vfS7y{aUPEC=;ivzf`=)(uOfYo-+|V; JRZo1M;16TOOw|AY diff --git a/src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc b/src/mini_projects/features/dsa/application/__pycache__/algorithms.cpython-313.pyc deleted file mode 100644 index 20fe735743689a34291bbd9ac2edc52062ac9c5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5822 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 diff --git a/src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 7441951a8d67b295abb7d6d83d7ba1dbdfc75a9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 238 zcmXwzy$!-J6ohR+5Fup&s)#NH@dAhm5Q;>opDgDQ3`rc>P7(HC0xAY!iL_L7NLc`f zpKIQI(&^oGzCR^S*ZWh>-F@`%7yX&ebLK*}M3an$gYztrBHfK&;98|nH7ZOV2AwP9FiSfY^yi?uqUu!iNxb-Ab-dr&1?e<1#X(SjASlu-S% rzbeD56qXCCkO8=a1&FGq^_GQ7>l{~l>`MFTrl0*9Qc7DA=#hgrDq%*I diff --git a/src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/application/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 8c513a9d1a68205574f6230bc60cdf774e0f2488..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 259 zcmYL^K}rKb5JkIHGzhuFfU7}FT_6bVMFNiNqN!A3g_-WAx<`oj@DQ#%g_oGED|f0qT{$CMMa?9^YyeSQocZhv6) z3rPt&@q+P?-LT8Oa)Kmd~I2edjypeCPN5xpzMh@DV7#`Q2XpAJv3>jsv^!R)^bv2|D+QOk{47T;?bz zVc#+7pbpkLsS{e~r0X(Ic}8RW^+ZOOCK_4>`h|DJYtjG87}8()4KF%6(lGL==0x9DRd!L z63iN>HQ@G7LFYcXPADf6*gQv_C*UC0U9z(mD6$KBJnOkBzvKyf&A?P#izk&=W>h_$ zF!luC`e(piY%Z=DFPh$!lpe>`O(%>^UXP`y`b#n$cAMU4R8x{_G-~>y(PU~Noxr|7 z8vQ|9Nt7sJG`bk4x{-)$s+IyWJg7J-JgARGbv$@1su%{1&!-Jlk47on7TJeCF>ee; zbd~CZYI;f4R6U*?e4eIms4+tyOsERg`d3zs<&-w~92-1u4RjhCOv2%!D>m<VvQpsMbiSgUZTEA|7K(A5`K4E34EC3$hATp$DM)5!tP7$-7z#LiOvHX}wK9`U0*4 z`SA;oy>gZ8Y$i8cH#q8&eXkb3ttv!~*=e{hvYSK8bKLcb+3ef%D8kYZcisRO+i-#34J6;BXyq%+@5z zGiRkl3ECjfZOfZMwH+#5B;vGXL`b4!Y=kV4Fh6|}g_ztGlbgMH?3qhaLNyGPO3Act zNb{;RE2XG(W#A$n#&oHQzG}Lb0rjRksV>CR$(sYu#updkF~IC-Igo~ZR8>@r=tnpmWruov2e z8xkx^R9Dpn9d$23p$$j^ivY|Sfa*}iH4WHL>wrg}fa(q@2(_EddExj@U2C>&Bwu$f z>pxcz>a%TU^TJv7a4KIno%K%_8d~2F3eDYbh&zpK8?XN4)vdXFpm~q&k_@OJ6A!jq*^jPb0fVLA(Fcj?mf<6)+!OcAs;w9Un~x zkB{`QXZq%`=@cM&Nl8e_)Jv)^DK9DUgfgE{2WT4zrX5f%{o(gmcErzV~XV|;7>sWm2e)Sj-G(ZbjOxc@t8`Ff4S9OCihMlwrAMx`T987`j!V;n5 zwUz-YfPj>)GLqR?iO~uITr1Ik0TsZsp=G)eV({LrySKK*uAJDF7rVENyx3Q0Y}vTA zervn&RIc&VgY)^uv)P)nC~ACtd~4|8Gi&47;BeMIjFWHQy}g-!=*ryAibGlNkcIv- zW?{F8IT&;M&oT5LxI3_eNcaj-;jP&iD?9PeZd9f!i^gehpFPwfH*Xixvv0U$1zV~82 zIG**7@7C6Dw5_)l>Kc*n{QCJFXhzmY*1|hLXnIXWoiF1q))0Rk znoUmV+No>Lc8ulgE@b@|3SvD%JnL<@kX?zkGO~XQI0nB?W?;vMi{^oQw=fL%Bs;D^ z=-0e}P`tH6CE{gRhg=Zo7&Q(R+csn;-j7x_`=im9!Mg%yq)K-6gQwN1=E^&i@hQ)| zBzKhOx22V%4Pd@QI^ewVk(1sQ?Q>5i2xdOf$Z3g?&1PV?L%P68gb{rW2elRB%*>Ey~^>*_#!n&E>-KXKQPZr_QB# za2{6HKLK6cA;mh<(7Z9XK9}(o+Pfb(f1X^|9|kl2U7==M=*S5jTVr`)un;=79qP-4 z`tqUvhg>f7WM-luw&cZdp{9PbZe3X8cY@M`?uTkFIF|K~?TVp$uibrZTkOb*9Tr8& z1xK>}kzFB_6Qr$3{>0fs&ujC7#rnSS!7J~*@-UtYek1d2p|fYZb2!&I{AuUNS^!qt z5ZA@6z@wVsLaX%l)LT;zPJEdDcp`f>^2w#_cfXf!Ro1Es{ZFj%8^XGW4!g^V#RW^GQ`!zMgM-F>|R9KAoAkH+gq*Gm!K56pr_1CLZ~v zLg&CEfBUZ3gh6X7^ltMz%`Bgj)?7QmhBf2uYj443Pl7tc;G3_47NE@tu($Rh*jr5~ zBd{Miz9L6rDW;e-`)A&0^e#S9^`3?}v$H z0?P^*JH3h%8NHe^WaC5&-b^rJ+~gM%DaEj$9zi<8JI*>R314`_RTRSs#T<)b$e`dU z2*oHz&tNr(6Q$k4A@^(1`PX)$^g7VD{*ar zy@pVxV$>8n=ArYO-ZATw(M5`9M=z+Ou8K~M<31x@pA+GaB=8yO{EW2xg^a`L9m1Cm a2PYN@RQpaC7IFBcFUoPDzY(n2G5-y=4HMr0 diff --git a/src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/domain/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 61d33cd77c3b2a3f3a4612b848b2b590b9b08543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 247 zcmey&%ge<81WS*;%v1r=k3k$5V1zP0a{w7r8G;##7}6OvnW|)5@^ce2^AvLPQ&Mw^ z71HvH6hbnSLlToi@>BKvG#PKP$H%ASC&$OHWcUm+;#RhPXmM&$v3_c4dTL&3ab~W5 zKv8~HYH~@jeoks)QJ!u=Wl2VUp1u#569C~97bWZGX69wa7r@k}r6!h?7Nr*Jmt-cF zBqo>Sr|PGGU7{Z!4^&tZAFo$Xd5gma=&sV7RJ$TJpxq!p6ocIVftit!@fL$p5etw5 E0NGqg)&Kwi diff --git a/src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc b/src/mini_projects/features/tictactoe/domain/__pycache__/board.cpython-313.pyc deleted file mode 100644 index 130a3216b2feb8d3728d1b3197785e01aef749e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4356 zcmcgvT}&L;6~6Pcv%l<`#Wn<+dcc^lZeVeU9kVzP*A7Z;z(3=4N$qqp><(nueE0m_bJr>=0tDJGe!rTryoCG>nR4^E1h>`?#9g8gg*#8qag>vh=Fjuz z1S*^psmOTYyyu)mrE^~DmC1FXhBOdGtR;%)IPb1=+J+4U(0GVy$Z@CTacm_1VXsAFhiBt!IE9uNV6k)%C_Q$u2HjPY~21ZYsPyS zbJ1linu?ER%#1o#mL1YHJ4baZZf8=UTsy1B)7eoiW5x#&YU|jz<2z+&ql0O!%Lz01 ztz{lDS?D5@WHGgvd~)gWdvp)5V7W+IAJTG$tq$RubbP{(cDiQS^nJ&Z(hbAvpQtDs z%^eP>a+K<(t=`O-rcP5FtW<6i5KKQ5h`Z$4#$1wIE_qN{6raaS^Ht<3ca^t~od4Z8!w7 zRtt2KC^}COYL~WXY{R2ukbwB)KvO_%Z=EpENN5c_VjSDi0!M3svrvVlE%-8)s*jG@<7EdZ z^XQEh?sAf>M4~h4dn0#73K2Q)lbIiE0fb6nCOiqmU9vlz%P6(2DO}l{WuQC*3VxK$ znKqR{elyxiFpvvZwLZ#e;M-2MtMVNaQGB%;I>cXfdt!QGIdZ5FIW#X6BaL}qqq~dE zm_!g}yT}6pyWqf)V3jmDDA0W3$AoA>vdKXep^RSV!0p6N(Can{&nAgo-8so&wpFCW zO?VgHANCH&ySaUm!;}S@EhT4QHrhb6oNAj(V;W!{kd2Mq4f|sLeu{pxie5EwNMYKO zM{|}f59;uIa zyhCZ0y2g{-D5v{jk(j_-&XHVuIg-Iv+ZspyQSIAq2sshgoErleh}JN4V8&qO%;}F4 zv-{`ke_H!AdN5ztRg8A$L)|bjTU!hsW@E>T(G&U5iIrgFc49g)GgJuH%~utJjVo0( zpVl3|9heTxoGAEealUi9bLPf!u&EGiT4`ur3LOGR+36OT(3x8_fKtB2yN=NUv?=WF z=t)80Fu%i-$EbHXk(lDO4TB{4H#kLr6U3Jkt4R5TEIZ4F8POp`bi@`gsOh=3Vf>eY z19?!g_)-=Gjb}gDEf)-2bx|N5Tm*6=EaH9@-bJ!;RT0nw(<&}c$bhHhq!)*`G%paeXe)QdkXV%v+4Sv6JFL&kt1D>J*8GL#IKa0ANWfwEW^MZCjcNC{K zA!>ivQ$p0UP7^S>iW!MzFvuuKuOd#0k@98YjKlZVUgC@<9{cBWvpqlQe(ZN~0=iQO z>@Db4AvnHNbg$!=yRk!+^#B%3pN^mzhU1}{IjqM5EFDwK1I&jUFU{UmGiKTmfN;F2 ztid=g<48c~MolQqGuki~;KyE)ud~`vmOp@62pe-jijM7sHalMfj9BZ?eGd6MS~Gk4 z&R3_rE8*zvk?E1y>iJ`H2OfAA>lcEH9~E9bnGbhN2~UIJrEuHAnPRwYIoMVRwyjiE ze+q9NyK{8P`}ECIPcHrPR-n0*(@1nFnt0?d zMia}CL?Mz`sXp*u39m%1^+@?UTFfggGHcTj%l?!gY#Ex1j!@%Zh$piI~ZaYEO zAdF!lSgMNlzcQ1Yy>#cjDe;Gam9I2RiGTDR*jd|Qre@f_3B(td=eyM>$qYnJ2w6=_LefTre59~W6-^Vw-fo#yb+#;gV#QE-@L?em}<3E3k?q4ra4PnGs+nCb@BQ z6=vPw5#Z9pglQ7S%CcfVKr1n9H?cjAPgPBAR996epu%M#or61r>V#Ak3NfQJ6H(Qn z47F?{W9nvBRVnJ;)dWm!x-Y{Nob-g&VONhGW^{_liNzqri#xpxowH9>Excu?RL!<& zW-w>#7QBwty{iT+@m-ap-m%!RPDA$t^4IEvg_f>Db@${uQyoRId!?~?vVW$pC^o+k zIZq$=LgGC2*d2Kx1wDIT)J8q~UNrNb!=L#$&t=YC9oOuS`Ek=jIlPR*xA6pUf*bdJ z-J&d&<7OSn9v{n?!xa65A{ZCiR=99$bv4*U2SEg5!omaOnZR+}pGm{tN%LPw{WFR1 W!O!@1?lSj`V8157z(3dk1NTpkqIj49 diff --git a/src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc b/src/mini_projects/features/tictactoe/presentation/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 53993fbafeb8255094bd4c6fb8c4fa6ba9d4fda4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258 zcmYk1K~BR!3`J)sDpjT2A*@gq)E7XU0HH{PwKbZ!Wl%B`JkBEAgF|o(7F;6RuDV0Y z1v&vL^>6&2^kw@`S+-V5U*88;q`9BrB>iuuRWcvyNwM1L;^O_fWQyd)hzmY%%s`xP z1Lu{qm9bTz&10d*E5tfj#7{NNd&W0-j>HkD-;t;2S|~ZZl9dPz8TPl`ATPng!!MM7 z5ZOZOyueN0y-u^m!$-LMo(GjtQWft+F~*}P)Xj&Q0# z$CW&tH=tz)l!+vK z&@zm-zr&5E&>Ioa$u^hgFmQD_523rhzQ=GC^q8J|jCoreV?_Xr_;!-Bd7|jUs?BHB zeBQMA0?~BK<}MkUo@8zQ0+ox6HJc^IHIp(CMbXZuGp0(+v|1cib=A~H(x-`WNzGbj zIJiN3*OuzRk4ahHPe=s1vOcQnl379>v{-%8)c4UHG79tb5$;|ubZl0TF-;+zxozqq+y8G|K%l~3g{OkDHN3kO&75@)f zx|_HxAeBaUgv;Yq1S0%gW1!~1md#-=D07X`WXfuX?;0cUEPI_HMPVf#q@s3Orebbj zF!8FvK<|rgPiJ&kk)cMo1J<%HU_z{CFfVFCjxn)1*3l3m0XSHaBVqX1tZ5MfM@A&( zjN14TsiB0hUL3iglCs)rVaVhFfcc1|j!uZLQL7A-O|lt=DHqcK0J`nz`{R|rCKl@Y z|D5=+`{$XNJu_GCcFpdYy>hSXgYJ8~7upWa*B|<*?cg;2p)^(Vqcf$pgA4VC{*)L$ zQL5|zN38k(seH>bP<+E-{A|9i-)w|EZ*1*8;QLiWcUSm6_5*sqiwC&Q63TkPtGi(S zEp#3cEFr>3n6Pc2+w+{nz62D>FL6$s<0YQ>hJ8uF4jn6wDEjHVGOCit4KRn$Ep1<3 zwJeon*G3L1DA)+EJ_6(x+P2P+o)=@HU|#zwaiPBwLtuHid7)KjfLkT1RAY(Gp54q$)YnKySr|W;&e5ZLjGS_izZd=cM z;`n0XbSZIqKG8qXv%+wZ&C8pbruM)0?MZG$K=Jy;=FU=c=X~?t`Pkm&c*9)d-i7$S zkK>7@x=je{BVaZvDf=W{ z;P|9#NWq*oG^AyVd26)qvxrwa41!&~WrrYX=8O^9RMquWgAaH(p8HMbyw$29gP!NN zFiLG~LxWMuXG4?lel~;hmHERc$@C=|@+Fu@UICDVNJDYz2GvoUQM4o`J7LfSA4cWq z8!>tInAB__Cf9DA%Kh~6?aQ;dUmN#~IqA(sDO-YHZEk}5 zzTeT}x==>rr6Uk}!dnQg2H%aXpU&FQs(EiG6n6BY2VjOB%3d@K({*1Mqt|k5+@$fd zoLXF~+_d3Jo}YHYL~|1W@Y7h`jceDhEyh|)vDWFX`B-vJNIG`_iP7!uGGsS=Y_8AH zRYF64GC*F251}$4DGJgQP^OL&$g31{Olc3rs1vxhO_Lshlf8~;-@~$?{Zhug?;NJTQRjyL9J_R5-(&!FyJtaE8@= z8Q=4g%$hXh2mZ7M*wD@vy;{lJAxD#u-?i7*&-oO{>#7>{4FU|!T6Pr+~5rql~%1y0WrN z_PJ*?X*B{Br_VI)5O+EOrfBXvEs7;Gv9o0i&k&zL`Z>vrM; zVIsOzvwg9qy;Rda(=cC?p71Tj>TkSr{hg_+i?LKGmV%w;Y7Z@h4}To4ea zo8Ix`$j!Q$eG|Tw?I;v4pJ+bV^rRK>!HJH)ac+`@%AHm_u99eUBi$4n29RWI?jk(+ z`jS3Z^0Y#A!`hcAR+*iQ(W+snNS|+z>1mj;LzTC-Y7&l)_^i=_rVkTZj@s+rekt$m zm#U~Dr4$NzE$h79Q;O!~GP`5li}87@APv(h(0v_J|8-2sc}ut@hZAzCiZ{e!Tlk7w z_8cSwK&83S%m7$nF~$#2>nEt?0c!pPz5W0ldg5bn$CDt7J66Jo2|Z<^II)5Ntg_Hu Zak{MzA%J(SAOK|oU{4cGc(W74{{zd=sGtA< diff --git a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index adc1c7069af996188bf2cb4c3573584558ec94d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 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 diff --git a/tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc b/tests/features/advanced_python/__pycache__/test_patterns.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index a9a49212f4d067dab6877f05607de0db3c894df3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4480 zcmd5<-ESOM6~FVbpR-;&c1hyY6x(qi1IesE8=PiCTTxnEWSUGCEG=R**&RER?#`_4 zopllek|lTu5)yf6q9Qgr|#aWh126n1|-a%9Q;)vQP9&cpX^O}DYSD)g-nxJ6ZDKbRS_?~L5RC> zHngdKNJv!<6>>El<@)I~(X$l^de5zmE{aQ`t)8!DBb%#y=OZmh`{TX@1@-3#Mkx&1 zqBae4T?$ti&CNVM(5m?Pt-de7ocb#{+**jnIMPy#EOkYb&cnVqvR=@KD)OwrwN&+3 zG@nFYqfuEug_WhEzM|9u-zy7_P!?MA8$>Ox`l*`Rc07tFx{u<4;Ac(GvzjfB`Dt*Q zHgyL41Xnr+{{uKqa#^@fu7RhN#j=_(2r(7d)r2qg=d>ZK zsa1-{x{vjK4u^NC2_7rDZ>xfz|AEL(miGD-s@duVn^sHQy-5`bgI2kb+@4I4UZi_3qnUgJPO?arqMSsGolsb_6%Wr2a7jmeVk()uJ`G*)pl+7ogg>-nJW_?SP{M3-(VM?1tNJ z)VZm#(QvPu4Z|^;mY*}2xo%+(e(@DPwCy_lMEx;i-K9;_GpyUKhUu6d)TkLgZ(qS- zf~n3WmBpm85GX+}FD8|5QgAxL5ZfmC_cFyu@u!YFLwXMEa@hD?5O7|5BPTk??*4T9 zjlZPw_g?9yj(;M`spsC+|3M%*kVyIjFZf%56rS%X6T9-vj?6_@nRzUSzlZY7t}+p; zLFSP>bN5?uS+}o2MQq)}{$isz^Lu%QehDTx5TY@Z{|4gZej@0q2=M$XL13LnLNFgZ8&j8ADwAWD@fO2aJ0J#7+ImIbZbiI8%u6+btt_BqdAGCx5 zZYcs#FdD{v6!QxZ&6(hWXR1iY!3v{$VJjK(2n=vH5;gD34IAPePmq2E#UzR;6w@e9 zqnJT)2E|uFs3|%NNiqcIDFS{n^cLyYp@boz@W=}SZ;S`tAPu3&6mT)@=ODJp$Hmci zF28r~w?c1hzE@n>etqxccxUe3=8mv4*V+6)_+V~lbN9HmD{COTiuPCze-CBt?uA$j zGLHtzx_u2Qp!pNlph72~6Hy<+kt8DuNfM_;5|W;)VKl)q;?EIu;eVt*DNcXV1N0~J z1&p7})5p(|mVNq@jpMKh7WY>{^@NNRWj;{Lox>W;i>A95czX02~ zH5^gEkS?l{FLTN=(zKm6;FSR^$Oe7_&nVAr*|i9G&;54}reOyIG|ZPhc$d+WFe05m z@nsOI)CYTtz`m8n=URCIV0{*QUyK+YWD*Q795_MrfWlF{DB$`)z^C8_9Q7CZI0MkX z9I(A*RiSvl&}9@)iTuGBIeH1(AmUSWJgTBVxToI$k-Qf|HWo0rrGPaCJZ)~4;T^~C z1{A^lFwO6Wy6Ks<1~f8E2bei^J-6mIj2l*?Wl{PnG{d`#i9m1Qx=>DZe)PLncfa$) zuCliM?LB4q*UP_L2I|!*aPb8Hy1N{cryzs$YcVcE`W0$e5gG&g>J(y|I0ME0`1Gz^ z`c=6z-cwFPZs#@^U8VF`4u22j(ynqkR)fqVY!sJu`x;bq#)FDpq9NpWZpZd<<_~g- z-&SzZBT}rYML!#{a(;>XlU@aopW)Kp9#yjBls_Y=i9jN{4u3P zk0mXNl<={nnXOjCu5rSyMQ^6Ssn8esV*4qv1GW{+X q;lqw0<@iJr1mTZ_{DSPW}xS)>)hY diff --git a/tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc b/tests/features/async_concurrency/__pycache__/test_pipeline.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index a1e7311d90db1dfde52b6c5ec9eccf5109116f8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4678 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 diff --git a/tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc b/tests/features/capstone/__pycache__/test_world_bank_refresh.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 94f4933f0db87d21ccb614f7c85a776dce562a0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5045 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 Gss9D*iCLHc diff --git a/tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc b/tests/features/data_stack/__pycache__/test_analysis.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index e57fdc97125e99a26955d6f4d7930f24c0f3fa7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4225 zcmd^C&2QUA79UE~CnecV;y6y7L{4JIY!XTSNLr+^>ulo9*6zA)XkptLSb~vgg^f%K zLn>MO!oXELcO_1Y<_<&_O&N3BiqtSG9 z^l2=9)gojq?%#JW>2tm7d7|`{ z<1>QN|K7q(s{fgNq;T>szXN23Z^(KG<1l>2`>_ZFL@-Q_xgKr^Eu@tMn@U)Vw8BfV^kyZ zspYNoRB7t^)zTzP7Q1g*5ADM$1nCgVmn<@|3Wb6tgL0AZ$^eU7w5n4(bpI5K)u49R zhtKeS{ER}z>BA}1;s?&Mj^$`|)my0sDngz&=XZtgBeS!+!q%~|iN@xnZ0BcZ^T^J~ z_HKklL5F1K`eatF^EtFc44!aTR25tl@m2;H5+=BN$^zFJwPceOoxRjS8$(lEiAldr(o}Sf3Uw6 z%Mc5@s5fi6soTazafMnR>J`T>)^&}VSK*8gEVKAF7gk&WPi+_1bPZk zDi4cpJWQ>=X&ZK-vB`$GhI_z4xmsPXTaW1^Y(s*dy#ViYn>-&l^YpbZ>}Og#xzha^ zPoKTpPQLRZA@;?$?|zde*)vc6(#ec%FMK5pJ&ARsv)gyR%8l-fb#fEicb`jrpZ2w- zd`pyf7C323@`32hJyC8+c~{&I@g4LIyaEQ)U05&Imd>@r@ttW-+S2%e=*>NGyd|A; z#r+W9LGQpTU_jlKj(u|TWKhW2bGh*3#|H(@v=wamtpl3ht**(TjEM?GJ+ZN2Bhx)*od9x8S~2jd&U5? zj=0b|<3j6Jwrm=XUd<~G;3+^>S-G_}c=ygq9&q8Y^B_N=nPyX~=f~u&f#vrLU@820 zX?3Nrbf@saT^4t}YXAHa2btd*2Gr(DPnK3or5k)+=2T8n>?T>FVl_<%8hrS7z!}xB zt!b7DI(?Q2Sk~9jpPiHvp`tZKZtwnA-ljLhAD^o2E+9G20(r6{V8@M6!Vkjk&GfakK_W9i%7G!qO6`*a(NgUci2gg?|7S+^k*WSok7N21mO?(#4i| z`Dv7swsiSG^yZ#;xg}k6#r+W9LGQpTU_gy9>B8=%o!c!DNLvD+^5&j+iNh|6`ysxA zj)7Oefcob!IrsGSf4tXvb*?4O0c}fj2ckFkK!ae3;(mzlaL>RkV8DlBKPg98oa)fU zn9Fh>OWto9b;mGmmcb?~kcL&a(JQ!Z7c^@Hsz0XiJ+f4w!oH2`c!sd+8-{794X>9f z_`Oo0*DDpYMx%~@uyH*=xf j$TlYYi>M$7|03ig@*PPC5l+52krAf8J$puYli&Xz#l38$ diff --git a/tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc b/tests/features/dsa/__pycache__/test_algorithms.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index dae40e770d92b5d5859410826e75d957d2c0e267..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6252 zcmeHL&2QVt6(^<9CvD44oU~3-N3r5Xm^PB-*iN#kgH1QdZXL(ngb|Qk9|*>x9i=j< z4CQX@U;!5`PzOD%fkgrJ=4(&=CwkahrIi&KM1dCQNw*f>qCijk-jG95{#ZAQ0PP_J z@#f9Uo0&K74f%U-c6)o10z8-g+$nt=7KFcJg+C#e*?kDi?*v6q#Jd6&Ss9uMTcNZt zLue$8J)yhNnHY`D#A$pcK@&4cnuPYK5h84mm>n#;yN@IPYc34Gd@Z@=<($^2Wts@ExwX=vTIZym-WlwsGo_TlfTBn z(%#OGl=OCfq-2=ut&l9a1XeM~`~cT6F!~s8^Gnd5@X@@~;m}QshBz%&GKYF6a+r61 zD3}FqKjtYomTx_U76)7J_f+&XJQd|0;-&8?BLv=>1aA%czU3TzONV-kc_{YQLyoNu z^w1momX-uLK4(WOtJNo_vG$>6SvswzM=w-Xr(`Sn!e!FatZAG#0pm7}$yV0VHM;O5 z`Pl`Oo{%j$`I$|s@HdCQdbqMWhGZPc1d=J6;A(5L?plWV2G(1<_uVwFYP4#ppO&iK z>dO*Egs`I}1MOi)oh{lu?^RaS$1A0B6*}x_*<8@dmYsywD#DXS?eDAB6LY0pRHQdQ!td&(mTLxrPEp1W9X1nhuGcy3)ZAwjQA6FMmx~x@I{duLV8Cn&3Y{~MKlVdc2 z`bfCGv(tRAayeL;bSlo2SAsom#p$_fAylTF$No51fSp=7ePu~EbgQ(S`-qxP^@XaH zD}#HDOU?<&&9cfz?6~A`(^hU#*QzU2w{k^GV@;}7UNR}zblJ*QR_zgH8$1ty-)K(N zv8JjSMYTfp#nN+{232r})(T+oy6_h%_1mfSTRSBFo4dcfyBXgmLpxIH^Xo6K*GYQw zIgpL(o6nn48n~8}Zj-LJ0o@Z`TcFm$aZX1Q=keJ3zB#r@dM9=K^A|5))X7L~3dqKb z+Eg<&0^GKQv?YzSN!Qx|p{mywsI_pk)6vA)JhqY#WltdK65gCnkx5mJbm9}ym9xU_l8!}32t)D=;p|A$x}4a7MA zgQ)As)x8KyiDe@!K8w5t>PI-*^KuXBPaGPfp+Dn?`4i%_#V+3_-uf25%kDVsJNP#C z8orHjpYpQXw?-JEx;qdzBr+z37J# zKjL8s#uLdE%nQ{;NDSo&#e{}RNHA%ly+AOuxZ1&g$l4=U10GqtF)idI#RcpLQ^rns zN>j{iD5f&>I1&ta=?Nr|tO@jcK;$@m2l*i+r;tGQC(s`N3F7s&AYNZ{0uBeS^J5nN zkzK$})3Z4848vC6Hma*q=@+HyD$RljYG%FXpz6S>TKoYr~uT2IUWN`C-P247@ z_SK~mZRu=7I$I~BTOyJ?kh*ksOKeJ`z_z5(HtBjBWOUJjMDh$sJ(bzI*-Xj6Zc9j8lH4X;Z-dAiYhGKR)?&k*o+eJ} zv(G%Xp6Z&+X@^PZIk03GKzdgVS%5dV9TAmK4uCHwDkLZ70GP7G2wXrE;5LLqm?0KK zC5E35`yGxK!tZ`42mxR-A<5^rF9%*mj0j4iKqf>b`6x3HnsmW4Fvh!xfJNK~QVBnS z_h&y?(oYMeY{&r!rAO&4M8-r`i_T-f#-6~c-c+e-4~76JbuRP zDR{piBMe~it3sSv=l32owErvXB)~eUM^}C8DE;#u6hG+atNG=g>lygM0y{hKqGH~= zwBbKXuA>IucwqE_Hr_uPu50>_8s%H(NJ-C!M@q)I{>p%RU6WLfDaQ***W1!-cw6Fg z@zMp57+l~Wc+>zq2Iu6EE&ha_2kRsEK1IXHA)1GRj=HV4oPJ`9H|ZF5u-9p|?S%$N3%>Rc&go#P6jPnPPRLQW0$ z2wsyNbK9J=l3y+vB}k~9v^~qy@+_p~S*=njmloK?Le|gD*(C=HX`Ca6CzYk*EOKBz z`-8xgb$lED@v>Q5DeE`rJ&?qZ&%*EbA7N1x|0)Q-7XB?n#LzC1ulpn7c_bJA)t?aW IiENf{0CoUE(*OVf diff --git a/tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc b/tests/features/tictactoe/__pycache__/test_ai.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index e5944b5498cb8c643879b9646d50d21912e2341c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6300 zcmeHLO>7%Q6yCMhzncWgZvd5=&`=u`JI-I51P9X6pQNa?aV#oDu-e#@WWnCGW;TQr zA@#taoH$hq5{Dj}8{!nifl3tzZZTFkV4Pl5N9d(U$g>07LM^1Rfc{fvlWnmezyhu#Kj7sJ_wLTz4n`O#Gr)D3l!jffX z-sZx2SHN{)nFXTT%apK;Z4_<2Xj>%XE|`(7t~!S}k3Wj08=9?uNSL-_n5JPa^3O@L zU`7b;otr=|OZPes{M3E2)^Y0c$$PP<)=peMbwjSjj$b~0SMB^_;`513rT5CjTJM&c z0Pfl(CpESAj^bTgO7Etc@I{xo4V?!3x>{Yrh6%sHWo{|GYxVI!SxBJl4-L~?n{4R_ z;?GL&4!nj`L?Q|GWDIiU!6zzu^r>E&enTI3yL#7#(3Mm=7 zO|-Jb7)1!fryjt827xr~NVJ9T$Qy9#?+voG1#*GE0dj+lK+Zbe7VMp$!G&rm)pzYQ-lKD-k(_}Ankg&07==*2S)P%t*<1HhTK(C#59 zSf8amO!N1wu#Q8)dN2I;Ed}1r?*%(O*6=3nwS6cBuyMcb_d(sB?Du=|lWMoOt?yrT z-|hEd@}BJXdvPecoBbgHCjY1F9~SoS`ltEuc~{M-7!yEyoWt}u2kmhWw5xLn;dp5n z3$a~Gf!&~rvBc8@?Eu(K1@0K!A-KZ;;6L^NynEWVf$Xh^G=ALr`Rph}XRF)1A3vaR z$ED;;KkhF)&yEOKkDQ0=M@1blUe(dIP(M2)G?DXEvwoz2oT%inLfJAgN`UYHE+-7I z6Dqiy;ex$lrQ)<3*Wo!N5JyWC!dwXhgri7Cxf9}%!2qX2oQ_;-3LY@#z~JFns-qq} z&;$+?DxwYaB^^PuH~`;jR^b2?Xl~(@F$Fx{6^M48T3$c6*7(a`X&z zM_rZ&}sYh+734D6lcSS>jQ z%=hEpj>BVP{JR`#B*!*#TgfTTPSw<@JD|$1E$n{S7hUGIGIeFdudCH1Xn@K57MHoD zOsx^W4zz{DwOhvq?d#!2Z6Tp{a?Ee>$KDwB>tJjlf%e9@+W@byk@MNjWc}s*j=;dY z4}%xn!H>cTSOn=&I00)g(;kTVAgLio)Lyd=NB+Hhlk2YPK?L_o?YtLn@WTlwj3|G( zLNS-Le-Xt1Kla?A@!+dc0y!E6N1aZKOIlTjV{c`Nm>Pwo1!RD%6-Lj31?WTAO(4xD zAWvJIx$#mh_SXL=ZNbl(RKN*S0@)n%T&l~7io6zgU(R!yn+Z$19tg5iX-Zb8VMA8S zuW}+@9d~|C27L#tr<+Pji&RnCPZg#0YPD<>d9o|58*T!wZ`WRsRMZVK&9i;3^Tjk0 z5Bs=p;-6>Tjr?5K7Ja>9m6pq7g3f{_<}4Yy;y;42EdMG=pGmi+)4xb>+z$ukSMGOk OqR7+opIyN6wfPHaR~fPZ diff --git a/tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc b/tests/features/tictactoe/__pycache__/test_board.cpython-313-pytest-9.0.2.pyc deleted file mode 100644 index 1ec531c2b82c18dc9962af8c82a7fbcfa836364a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6388 zcmeHLO>7&-6`uVe#pO~`T*tAU+SNvmWv^wEqV;3Pjw~yU6UQ-R$7>a8bXcs&k<5hL zWoMRg<-$OK_E5J6qd*JzC=|KymUC}C^pKlm#a6H|kVBAzZUN*Xh)#KL_J^b`{}m`& zw_cEM-kW(d^Y-oRe&5VScXyV+b?+bRl`j-RzCk0t?kUa_Cd>>G_dT-8#% z%hRdz#G5}Dd6$16WEMsbuS@AzYnA=PQm4}+l9ie{Ga`>hzE*lR6Z>2e35~6=>?d)I zC!wPJ(~enkoxW)G!<~#q_ZP?JywcID=kdKRNDx!&PPX=z;yIq^DL=KR^3_6c+Tju2HG_6_*F;ss|oC*k$u8UZYwTuJ%IJn{%qR>(pqV+1#0@ zI6}}fDZ;3D?it2B7HqfYJyvskoBq09bzH}X7E@yg0fCOwgkcxkjp24FpSMXtB8gj)8w6Q>4$}6?PMdp1(OFl1Dsl(hOkew@@TR1O_ zi=lz};)QwY_zgyR!LO8jr{sIIAQCB_Qog=R~^YDktC*!JO>yEgw-0}{*zARNRb zj@b7pxtH7a$9F$H^OgS6&50Wm%j%(9MHD3vzn{2OT-EzQTPlY_KeVPszuW4eWxYQ! zL*)*18cpg}S{61;gbk}WMN2IuYBC&nER~Z{+7%cv33Lzx9mGHflRyWTD3~Dh!8JAd z-Bt(1$gLV^x&xgc(5P;uWnlvZiVdqUIs_W45NM<(!-2;V4XYoJFzE*5m~72GUIt8U z=N<(hMIT6({D$-x_Mrg#P#;2!)W3unX~90aI@m|%zmR=oo`?g_AN$Y}_K~&pe>?kl z8CPNF1x!};{xq$fI7^7vA2Y-9-#VD#)~q!*Edy)qp4r{TTL0JAjTUn}t0%7rrkJ&Q zt(T^=5pUF=fj8>$SaI3njdc_7ecKe^QwkoJ5P@Vc6OBg+-~g!-tycLuvI5cPGQ%gw za3djrj^RdvA+u#jWt-BbJU$MrVeBy!q10fEy1sq2;<}UpCm0VI#i+@f3e#~c+-@KP z7~Sk?!~SCc{ZM(VsSDI+xcJx(6tL|eY$u9cD0ZWG6$R{K2-^c9=;@rcnG56Jiw-|S z(TBo7@fwPL6#G!T4x$~7M?xcnsFx7C4Vy)Q9C<&HXl@EEzQv@B>y zyjO9@m}R0r4rNZAYMgCvmn^ZLvi?-$UA>Fc=blFU}bnF z_waWQ!5BBd8-E4SPWj`bJeSNOc8lv$a?}>r;33{@Ld7iHBFHi^@Q6}t`NW!~SZaJQ zDb7>jOtob%kk`oJ`Qr8$V9Jg#7N0I@X z=+c#SoKldCW-8p(r^B6mkY-MWQyyf0;Zz$`Y`z8dHzk4L9Ofbs((Zp?i~5OEt&|<8 zjJBsXbKC{W&O#4l3LKwPvPr$C_1@h3hrNHkwrrkQ)lPnu>Au-}qxY6{Yxv7d-}eg1 zzPYBIysMqu+(44@+4f1I{GDVcd%8NX=sA2QJI+(zr_87|xNpo+W5)29aef<6RK#>F#@Ex~pPDb{;w)yW;pRY?bKKtJ>tliGE93(%_}#m^c1@c?zlKP!t{5F iBuQTr@;ma|?r+J&{j?$-zMoU11Bo#H38o?5@*e^F=