From e842f401afbc400e227205874a207120b582a80c Mon Sep 17 00:00:00 2001 From: mobasi-team Date: Thu, 5 Mar 2026 23:11:06 -0600 Subject: [PATCH 1/5] build: require Python 3.10 for Pillow 12.1.1 --- .github/workflows/python_runtime_contract.yml | 29 +++++ README.md | 2 +- .../test/scripts/test_runtime_requirements.py | 28 +++++ .../plans/2026-03-06-latest-pillow-upgrade.md | 116 ++++++++++++++++++ requirements.txt | 2 +- 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/python_runtime_contract.yml create mode 100644 admin/test/scripts/test_runtime_requirements.py create mode 100644 docs/plans/2026-03-06-latest-pillow-upgrade.md diff --git a/.github/workflows/python_runtime_contract.yml b/.github/workflows/python_runtime_contract.yml new file mode 100644 index 00000000..f4624408 --- /dev/null +++ b/.github/workflows/python_runtime_contract.yml @@ -0,0 +1,29 @@ +name: Python Runtime Contract + +on: + pull_request: + paths: + - '.github/workflows/python_runtime_contract.yml' + - 'README.md' + - 'requirements.txt' + - 'admin/test/scripts/test_runtime_requirements.py' + +jobs: + runtime-contract: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Run runtime contract tests + run: python -m unittest discover -s admin/test/scripts -p 'test_*.py' diff --git a/README.md b/README.md index 20f5b58d..4534313d 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Details in blog post here: https://abrignoni.blogspot.com/2020/02/aleapp-android ## Requirements -**Python 3.9 or above** (older versions of 3.x will also work with the exception of one or two modules) +**Python 3.10 or above** ### Dependencies diff --git a/admin/test/scripts/test_runtime_requirements.py b/admin/test/scripts/test_runtime_requirements.py new file mode 100644 index 00000000..20a0d6f7 --- /dev/null +++ b/admin/test/scripts/test_runtime_requirements.py @@ -0,0 +1,28 @@ +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class TestRuntimeRequirements(unittest.TestCase): + def test_readme_documents_python_3_10_or_above(self): + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + self.assertIn("**Python 3.10 or above**", readme) + + def test_requirements_pin_latest_pillow(self): + requirement_lines = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines() + pillow_lines = [line.strip() for line in requirement_lines if line.strip().lower().startswith("pillow")] + self.assertEqual(pillow_lines, ["pillow==12.1.1"]) + + def test_runtime_contract_workflow_covers_python_3_10_and_3_11(self): + workflow_path = REPO_ROOT / ".github" / "workflows" / "python_runtime_contract.yml" + self.assertTrue(workflow_path.exists(), "runtime contract workflow should exist") + + workflow = workflow_path.read_text(encoding="utf-8") + self.assertIn("'3.10'", workflow) + self.assertIn("'3.11'", workflow) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/plans/2026-03-06-latest-pillow-upgrade.md b/docs/plans/2026-03-06-latest-pillow-upgrade.md new file mode 100644 index 00000000..025877e1 --- /dev/null +++ b/docs/plans/2026-03-06-latest-pillow-upgrade.md @@ -0,0 +1,116 @@ +# Latest Pillow Upgrade Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Move ALEAPP's tracked runtime contract on `main` to the latest Pillow path by pinning Pillow 12.1.1, documenting Python 3.10+, and adding CI-backed regression coverage for those declarations. + +**Architecture:** Keep the change small and policy-focused. The repo's Pillow call sites do not need refactoring, so the implementation should update the tracked version declarations and add a lightweight test plus workflow that fail if those declarations drift in future PRs. + +**Tech Stack:** Python, `unittest`, GitHub Actions, requirements.txt, Markdown docs + +--- + +### Task 1: Add a failing runtime contract test + +**Files:** +- Create: `admin/test/scripts/test_runtime_requirements.py` + +**Step 1: Write the failing test** + +```python +class TestRuntimeRequirements(unittest.TestCase): + def test_readme_documents_python_3_10_or_above(self): + self.assertIn("**Python 3.10 or above**", readme) + + def test_requirements_pin_latest_pillow(self): + self.assertEqual(pillow_lines, ["pillow==12.1.1"]) + + def test_runtime_contract_workflow_covers_python_3_10(self): + self.assertIn("3.10", workflow_yaml) +``` + +**Step 2: Run test to verify it fails** + +Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` +Expected: FAIL because `README.md` still says Python 3.9+, `requirements.txt` still allows Pillow <12, and the runtime-contract workflow does not exist yet. + +**Step 3: Write minimal implementation** + +No production changes in this task. + +**Step 4: Run test to verify it fails correctly** + +Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` +Expected: FAIL on the intended version assertions, not on import errors or path errors. + +**Step 5: Commit** + +```bash +git add admin/test/scripts/test_runtime_requirements.py +git commit -m "test: add runtime contract regression coverage" +``` + +### Task 2: Update tracked version declarations + +**Files:** +- Modify: `requirements.txt` +- Modify: `README.md` + +**Step 1: Implement the minimal declaration changes** + +```text +requirements.txt -> pillow==12.1.1 +README.md -> Python 3.10 or above +``` + +**Step 2: Run targeted test to verify it still fails** + +Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` +Expected: FAIL only on the workflow assertion because CI coverage is not added yet. + +**Step 3: Commit** + +```bash +git add requirements.txt README.md +git commit -m "build: require Python 3.10 for latest Pillow" +``` + +### Task 3: Add CI enforcement for the runtime contract + +**Files:** +- Create: `.github/workflows/python_runtime_contract.yml` + +**Step 1: Add the workflow** + +```yaml +name: Python Runtime Contract +on: + pull_request: + paths: + - '.github/workflows/python_runtime_contract.yml' + - 'README.md' + - 'requirements.txt' + - 'admin/test/scripts/test_runtime_requirements.py' +jobs: + runtime-contract: + strategy: + matrix: + python-version: ['3.10', '3.11'] +``` + +**Step 2: Run targeted test to verify it passes** + +Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` +Expected: PASS + +**Step 3: Run full verification** + +Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_*.py'` +Expected: PASS + +**Step 4: Commit** + +```bash +git add .github/workflows/python_runtime_contract.yml +git commit -m "ci: enforce Python and Pillow runtime contract" +``` diff --git a/requirements.txt b/requirements.txt index 53629dfe..9120c9c2 100755 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ fitdecode==0.10.0 folium==0.14.0 geopy==2.3.0 packaging==20.1 -pillow>=10.3.0,<12.0.0 +pillow==12.1.1 polyline==2.0.0 protobuf==3.10.0 PyCryptodome From 4d1c4cb0df2e794ebe995c73c9175807a6a1d1b5 Mon Sep 17 00:00:00 2001 From: mobasi-team Date: Thu, 5 Mar 2026 23:14:23 -0600 Subject: [PATCH 2/5] ci: validate Pillow 12 runtime contract --- .github/workflows/python_runtime_contract.yml | 5 +- .../test/scripts/test_runtime_requirements.py | 2 + .../plans/2026-03-06-latest-pillow-upgrade.md | 116 ------------------ 3 files changed, 6 insertions(+), 117 deletions(-) delete mode 100644 docs/plans/2026-03-06-latest-pillow-upgrade.md diff --git a/.github/workflows/python_runtime_contract.yml b/.github/workflows/python_runtime_contract.yml index f4624408..9f7f2f91 100644 --- a/.github/workflows/python_runtime_contract.yml +++ b/.github/workflows/python_runtime_contract.yml @@ -25,5 +25,8 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install runtime dependencies + run: python -m pip install -r requirements.txt + - name: Run runtime contract tests - run: python -m unittest discover -s admin/test/scripts -p 'test_*.py' + run: python -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' diff --git a/admin/test/scripts/test_runtime_requirements.py b/admin/test/scripts/test_runtime_requirements.py index 20a0d6f7..94ba2377 100644 --- a/admin/test/scripts/test_runtime_requirements.py +++ b/admin/test/scripts/test_runtime_requirements.py @@ -22,6 +22,8 @@ def test_runtime_contract_workflow_covers_python_3_10_and_3_11(self): workflow = workflow_path.read_text(encoding="utf-8") self.assertIn("'3.10'", workflow) self.assertIn("'3.11'", workflow) + self.assertIn("python -m pip install -r requirements.txt", workflow) + self.assertIn("test_runtime_requirements.py", workflow) if __name__ == "__main__": diff --git a/docs/plans/2026-03-06-latest-pillow-upgrade.md b/docs/plans/2026-03-06-latest-pillow-upgrade.md deleted file mode 100644 index 025877e1..00000000 --- a/docs/plans/2026-03-06-latest-pillow-upgrade.md +++ /dev/null @@ -1,116 +0,0 @@ -# Latest Pillow Upgrade Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Move ALEAPP's tracked runtime contract on `main` to the latest Pillow path by pinning Pillow 12.1.1, documenting Python 3.10+, and adding CI-backed regression coverage for those declarations. - -**Architecture:** Keep the change small and policy-focused. The repo's Pillow call sites do not need refactoring, so the implementation should update the tracked version declarations and add a lightweight test plus workflow that fail if those declarations drift in future PRs. - -**Tech Stack:** Python, `unittest`, GitHub Actions, requirements.txt, Markdown docs - ---- - -### Task 1: Add a failing runtime contract test - -**Files:** -- Create: `admin/test/scripts/test_runtime_requirements.py` - -**Step 1: Write the failing test** - -```python -class TestRuntimeRequirements(unittest.TestCase): - def test_readme_documents_python_3_10_or_above(self): - self.assertIn("**Python 3.10 or above**", readme) - - def test_requirements_pin_latest_pillow(self): - self.assertEqual(pillow_lines, ["pillow==12.1.1"]) - - def test_runtime_contract_workflow_covers_python_3_10(self): - self.assertIn("3.10", workflow_yaml) -``` - -**Step 2: Run test to verify it fails** - -Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` -Expected: FAIL because `README.md` still says Python 3.9+, `requirements.txt` still allows Pillow <12, and the runtime-contract workflow does not exist yet. - -**Step 3: Write minimal implementation** - -No production changes in this task. - -**Step 4: Run test to verify it fails correctly** - -Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` -Expected: FAIL on the intended version assertions, not on import errors or path errors. - -**Step 5: Commit** - -```bash -git add admin/test/scripts/test_runtime_requirements.py -git commit -m "test: add runtime contract regression coverage" -``` - -### Task 2: Update tracked version declarations - -**Files:** -- Modify: `requirements.txt` -- Modify: `README.md` - -**Step 1: Implement the minimal declaration changes** - -```text -requirements.txt -> pillow==12.1.1 -README.md -> Python 3.10 or above -``` - -**Step 2: Run targeted test to verify it still fails** - -Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` -Expected: FAIL only on the workflow assertion because CI coverage is not added yet. - -**Step 3: Commit** - -```bash -git add requirements.txt README.md -git commit -m "build: require Python 3.10 for latest Pillow" -``` - -### Task 3: Add CI enforcement for the runtime contract - -**Files:** -- Create: `.github/workflows/python_runtime_contract.yml` - -**Step 1: Add the workflow** - -```yaml -name: Python Runtime Contract -on: - pull_request: - paths: - - '.github/workflows/python_runtime_contract.yml' - - 'README.md' - - 'requirements.txt' - - 'admin/test/scripts/test_runtime_requirements.py' -jobs: - runtime-contract: - strategy: - matrix: - python-version: ['3.10', '3.11'] -``` - -**Step 2: Run targeted test to verify it passes** - -Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' -v` -Expected: PASS - -**Step 3: Run full verification** - -Run: `python3 -m unittest discover -s admin/test/scripts -p 'test_*.py'` -Expected: PASS - -**Step 4: Commit** - -```bash -git add .github/workflows/python_runtime_contract.yml -git commit -m "ci: enforce Python and Pillow runtime contract" -``` From 882ce52a774441321f6d30fb16ba6e347d87cf4a Mon Sep 17 00:00:00 2001 From: mobasi-team Date: Thu, 5 Mar 2026 23:18:42 -0600 Subject: [PATCH 3/5] test: verify dependency compatibility on supported Python --- .github/workflows/python_runtime_contract.yml | 5 +- .../scripts/test_dependency_compatibility.py | 61 +++++++++++++++++++ .../test/scripts/test_runtime_requirements.py | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 admin/test/scripts/test_dependency_compatibility.py diff --git a/.github/workflows/python_runtime_contract.yml b/.github/workflows/python_runtime_contract.yml index 9f7f2f91..3716c7f6 100644 --- a/.github/workflows/python_runtime_contract.yml +++ b/.github/workflows/python_runtime_contract.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/python_runtime_contract.yml' - 'README.md' - 'requirements.txt' + - 'admin/test/scripts/test_dependency_compatibility.py' - 'admin/test/scripts/test_runtime_requirements.py' jobs: @@ -28,5 +29,5 @@ jobs: - name: Install runtime dependencies run: python -m pip install -r requirements.txt - - name: Run runtime contract tests - run: python -m unittest discover -s admin/test/scripts -p 'test_runtime_requirements.py' + - name: Run admin compatibility tests + run: python -m unittest discover -s admin/test/scripts -p 'test_*.py' diff --git a/admin/test/scripts/test_dependency_compatibility.py b/admin/test/scripts/test_dependency_compatibility.py new file mode 100644 index 00000000..02161016 --- /dev/null +++ b/admin/test/scripts/test_dependency_compatibility.py @@ -0,0 +1,61 @@ +import importlib +import unittest + + +THIRD_PARTY_IMPORTS = ( + "bcrypt", + "bs4", + "bencoding", + "blackboxprotobuf", + "fitdecode", + "folium", + "geopy", + "packaging", + "PIL", + "polyline", + "google.protobuf", + "Crypto", + "pytz", + "simplekml", + "wheel", + "xlsxwriter", + "xmltodict", +) + +CORE_MODULES = ( + "aleapp", + "scripts.ilapfuncs", + "scripts.plugin_loader", + "scripts.artifacts.notification_history_pb.notificationhistory_pb2", + "scripts.artifacts.usagestats_pb.usagestatsservice_pb2", +) + + +class TestDependencyCompatibility(unittest.TestCase): + def test_declared_packages_are_importable(self): + for module_name in THIRD_PARTY_IMPORTS: + with self.subTest(module_name=module_name): + importlib.import_module(module_name) + + def test_core_modules_import_under_supported_python(self): + for module_name in CORE_MODULES: + with self.subTest(module_name=module_name): + importlib.import_module(module_name) + + def test_plugin_loader_imports_artifact_modules(self): + from scripts.plugin_loader import PluginLoader + + loader = PluginLoader() + self.assertGreater(len(loader), 0) + + def test_imagetk_imports_when_tkinter_is_available(self): + try: + import tkinter # noqa: F401 + except ModuleNotFoundError: + self.skipTest("tkinter is not available in this Python build") + + from PIL import ImageTk # noqa: F401 + + +if __name__ == "__main__": + unittest.main() diff --git a/admin/test/scripts/test_runtime_requirements.py b/admin/test/scripts/test_runtime_requirements.py index 94ba2377..2688e0a8 100644 --- a/admin/test/scripts/test_runtime_requirements.py +++ b/admin/test/scripts/test_runtime_requirements.py @@ -23,7 +23,7 @@ def test_runtime_contract_workflow_covers_python_3_10_and_3_11(self): self.assertIn("'3.10'", workflow) self.assertIn("'3.11'", workflow) self.assertIn("python -m pip install -r requirements.txt", workflow) - self.assertIn("test_runtime_requirements.py", workflow) + self.assertIn("test_*.py", workflow) if __name__ == "__main__": From 7d21dd90e2a6f9c94a7ab2136888bde9e0faec96 Mon Sep 17 00:00:00 2001 From: mobasi-team Date: Thu, 5 Mar 2026 23:24:10 -0600 Subject: [PATCH 4/5] test: add dependency behavior smoke coverage --- .../scripts/test_dependency_compatibility.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/admin/test/scripts/test_dependency_compatibility.py b/admin/test/scripts/test_dependency_compatibility.py index 02161016..63a4d9fb 100644 --- a/admin/test/scripts/test_dependency_compatibility.py +++ b/admin/test/scripts/test_dependency_compatibility.py @@ -1,5 +1,28 @@ import importlib +import os +import subprocess +import sys +import tempfile import unittest +from pathlib import Path + +import bcrypt +import bencoding +import blackboxprotobuf +import fitdecode +import folium +import polyline +import pytz +import simplekml +import xlsxwriter +import xmltodict +from bs4 import BeautifulSoup +from geopy.geocoders import Nominatim +from packaging import version +from PIL import Image +from google.protobuf import descriptor +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad THIRD_PARTY_IMPORTS = ( @@ -22,6 +45,8 @@ "xmltodict", ) +REPO_ROOT = Path(__file__).resolve().parents[3] + CORE_MODULES = ( "aleapp", "scripts.ilapfuncs", @@ -48,6 +73,81 @@ def test_plugin_loader_imports_artifact_modules(self): loader = PluginLoader() self.assertGreater(len(loader), 0) + def test_dependency_runtime_smoke_behaviors(self): + self.assertTrue(bcrypt.checkpw(b"pw", bcrypt.hashpw(b"pw", bcrypt.gensalt()))) + + self.assertEqual(bencoding.bdecode(bencoding.bencode({b"a": 1})), {b"a": 1}) + + message, types = blackboxprotobuf.decode_message(b"\x08\x96\x01") + self.assertEqual(message["1"], 150) + self.assertEqual(types["1"]["type"], "int") + + self.assertTrue(hasattr(fitdecode, "FitReader")) + + with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as html_file: + try: + folium.Map(location=[0, 0], zoom_start=1).save(html_file.name) + self.assertGreater(os.path.getsize(html_file.name), 0) + finally: + os.unlink(html_file.name) + + self.assertEqual(Nominatim(user_agent="aleapp-test").scheme, "https") + self.assertLess(version.parse("1.2.3"), version.parse("2.0.0")) + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as image_file: + try: + Image.new("RGB", (2, 2), color="red").save(image_file.name) + with Image.open(image_file.name) as reopened: + self.assertEqual(reopened.size, (2, 2)) + finally: + os.unlink(image_file.name) + + encoded = polyline.encode([(38.5, -120.2), (40.7, -120.95)]) + self.assertEqual(polyline.decode(encoded), [(38.5, -120.2), (40.7, -120.95)]) + + self.assertIsNotNone(descriptor) + + key = b"0123456789abcdef" + cipher = AES.new(key, AES.MODE_ECB) + ciphertext = cipher.encrypt(pad(b"hello world", AES.block_size)) + self.assertEqual(unpad(cipher.decrypt(ciphertext), AES.block_size), b"hello world") + + self.assertEqual(pytz.timezone("UTC").zone, "UTC") + + with tempfile.NamedTemporaryFile(suffix=".kml", delete=False) as kml_file: + try: + kml = simplekml.Kml() + kml.newpoint(name="x", coords=[(0, 0)]) + kml.save(kml_file.name) + self.assertGreater(os.path.getsize(kml_file.name), 0) + finally: + os.unlink(kml_file.name) + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as xlsx_file: + try: + workbook = xlsxwriter.Workbook(xlsx_file.name) + worksheet = workbook.add_worksheet("Sheet1") + worksheet.write(0, 0, "ok") + workbook.close() + self.assertGreater(os.path.getsize(xlsx_file.name), 0) + finally: + os.unlink(xlsx_file.name) + + self.assertEqual(xmltodict.parse("1")["root"]["a"], "1") + self.assertEqual(BeautifulSoup("

hi

", "html.parser").p.text, "hi") + + def test_cli_help_runs_under_supported_python(self): + result = subprocess.run( + [sys.executable, "aleapp.py", "--help"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("ALEAPP: Android Logs, Events, and Protobuf Parser.", result.stdout) + def test_imagetk_imports_when_tkinter_is_available(self): try: import tkinter # noqa: F401 From c1c4a67552940ee6f9c04e9668f278bab88a2f22 Mon Sep 17 00:00:00 2001 From: mobasi-team Date: Thu, 5 Mar 2026 23:25:15 -0600 Subject: [PATCH 5/5] test: make ImageTk smoke check lint-safe --- admin/test/scripts/test_dependency_compatibility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/test/scripts/test_dependency_compatibility.py b/admin/test/scripts/test_dependency_compatibility.py index 63a4d9fb..4adf79fe 100644 --- a/admin/test/scripts/test_dependency_compatibility.py +++ b/admin/test/scripts/test_dependency_compatibility.py @@ -150,11 +150,11 @@ def test_cli_help_runs_under_supported_python(self): def test_imagetk_imports_when_tkinter_is_available(self): try: - import tkinter # noqa: F401 + importlib.import_module("tkinter") except ModuleNotFoundError: self.skipTest("tkinter is not available in this Python build") - from PIL import ImageTk # noqa: F401 + importlib.import_module("PIL.ImageTk") if __name__ == "__main__":