From d98e87a0d007d1589814d3b2e80ef59a44a2e027 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 3 Jul 2026 18:46:07 +0200 Subject: [PATCH 1/2] Install importlib-metadata explicitly in Modal images logfire >=4.7 imports importlib_metadata unconditionally on Python 3.13 but stopped receiving it transitively, so the freshly rebuilt gateway and simulation images crash on import logfire. The gateway ASGI factory died at startup, hanging every request past Modal's HTTP window (303 redirects), which failed all beta integration tests and blocked the production deploy of the observability PR (#594). Co-Authored-By: Claude Opus 4.8 (1M context) --- projects/policyengine-api-simulation/src/modal/app.py | 4 ++++ .../policyengine-api-simulation/src/modal/gateway/app.py | 4 ++++ .../tests/test_modal_bundle_image.py | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/projects/policyengine-api-simulation/src/modal/app.py b/projects/policyengine-api-simulation/src/modal/app.py index b57666745..e2729c13a 100644 --- a/projects/policyengine-api-simulation/src/modal/app.py +++ b/projects/policyengine-api-simulation/src/modal/app.py @@ -144,6 +144,10 @@ def build_base_simulation_image() -> modal.Image: "fastapi>=0.115.0", "tables>=3.10.2", "logfire>=3.0.0", + # logfire imports importlib_metadata unconditionally but does + # not declare it as a dependency on Python 3.13, so install it + # explicitly or workers crash on ``import logfire``. + "importlib-metadata>=8", "policyengine-observability[fastapi]>=1.3.0,<2", ) .run_commands( diff --git a/projects/policyengine-api-simulation/src/modal/gateway/app.py b/projects/policyengine-api-simulation/src/modal/gateway/app.py index e2a48825d..64144453e 100644 --- a/projects/policyengine-api-simulation/src/modal/gateway/app.py +++ b/projects/policyengine-api-simulation/src/modal/gateway/app.py @@ -29,6 +29,10 @@ # the auth module at runtime here. "cryptography>=41.0.0", "logfire>=3.0.0", + # logfire imports importlib_metadata unconditionally but does not + # declare it as a dependency on Python 3.13, so install it + # explicitly or the container crashes at startup. + "importlib-metadata>=8", "policyengine-observability[fastapi]>=1.3.0,<2", ) .add_local_python_source( diff --git a/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py b/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py index a41227f68..1bb8c7596 100644 --- a/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py +++ b/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py @@ -95,6 +95,10 @@ def test_modal_image_uses_policyengine_bundle_install(monkeypatch): packages = pip_install_calls[0][1] assert "policyengine-observability[fastapi]>=1.3.0,<2" in packages assert "logfire>=3.0.0" in packages + # logfire needs importlib_metadata at import time on Python 3.13 but + # does not declare it; without the explicit install every worker + # crashes on ``import logfire``. + assert "importlib-metadata>=8" in packages runtime_secret_sets = { name: kwargs["secrets"] for name, kwargs in app.app.function_calls @@ -164,6 +168,9 @@ def test_gateway_image_installs_dual_observability(monkeypatch): packages = pip_install_calls[0][1] assert "policyengine-observability[fastapi]>=1.3.0,<2" in packages assert "logfire>=3.0.0" in packages + # Same importlib_metadata gap as the simulation image: without this the + # gateway ASGI factory dies in configure_logfire and every request 303s. + assert "importlib-metadata>=8" in packages function_kwargs = {name: kwargs for name, kwargs in app.app.function_calls} assert function_kwargs["web_app"]["secrets"] == [ From 8eff8fbcbbd4d2d6bb1c53673c5ddaeee08e13d4 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Fri, 3 Jul 2026 19:20:28 +0200 Subject: [PATCH 2/2] Install Modal image dependencies from pinned uv.lock exports Modal images resolved their packages fresh at image-build time from loose pip_install ranges, so image dependencies drifted from the tested uv.lock environment; issue #602 (logfire resolving to a release with an undeclared importlib_metadata dependency) is the failure mode. Define the image package sets as uv dependency groups resolved inside uv.lock, export them to checked-in pinned requirements files, and install the images from those. make update re-exports after relocking and a unit test fails CI when the exports drift from the lock. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 2 + .../policyengine-api-simulation/README.md | 17 +++++ .../pyproject.toml | 29 ++++++++ .../requirements/modal-gateway-image.txt | 47 +++++++++++++ .../requirements/modal-simulation-image.txt | 51 ++++++++++++++ .../src/modal/app.py | 22 +++--- .../src/modal/gateway/app.py | 26 ++++--- .../tests/test_modal_bundle_image.py | 60 +++++++++++----- .../tests/test_modal_image_requirements.py | 69 +++++++++++++++++++ projects/policyengine-api-simulation/uv.lock | 64 +++++++++++++++++ scripts/export-modal-image-requirements.sh | 20 ++++++ 11 files changed, 367 insertions(+), 40 deletions(-) create mode 100644 projects/policyengine-api-simulation/requirements/modal-gateway-image.txt create mode 100644 projects/policyengine-api-simulation/requirements/modal-simulation-image.txt create mode 100644 projects/policyengine-api-simulation/tests/test_modal_image_requirements.py create mode 100755 scripts/export-modal-image-requirements.sh diff --git a/Makefile b/Makefile index 64b5e64e2..f8bf89cd3 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,8 @@ update: (cd "$$dir" && uv lock --upgrade); \ fi \ done + @echo "Re-exporting Modal image requirements from uv.lock..." + @./scripts/export-modal-image-requirements.sh @echo "✅ All dependencies updated" # Code quality diff --git a/projects/policyengine-api-simulation/README.md b/projects/policyengine-api-simulation/README.md index 9aa3c3031..c2f168159 100644 --- a/projects/policyengine-api-simulation/README.md +++ b/projects/policyengine-api-simulation/README.md @@ -2,6 +2,23 @@ PolicyEngine Simulation API service. +## Modal image dependencies + +The Modal images (gateway in `src/modal/gateway/app.py`, base simulation +image in `src/modal/app.py`) install from pinned requirements files under +`requirements/`, exported from the `modal-simulation-image` and +`modal-gateway-image` dependency groups in `pyproject.toml`/`uv.lock`. +Image packages therefore match the versions the test environment runs +against and can only change through a relock — never through a fresh +resolution at image-build time (issue #602 is what happens otherwise). + +To change image dependencies, edit the dependency group, then run +`uv lock` and `scripts/export-modal-image-requirements.sh` (or +`make update`, which relocks and re-exports everything). CI fails if the +exports drift from the lock (`tests/test_modal_image_requirements.py`). +Note that any change to the exports invalidates the image layer cache, +including the dataset prebuild layer below. + ## Temporary: prebuilt single-year datasets in the Modal image The Modal image prebuilds single-year datasets (2025–2027, US national diff --git a/projects/policyengine-api-simulation/pyproject.toml b/projects/policyengine-api-simulation/pyproject.toml index 21f382f1c..107cd6b74 100644 --- a/projects/policyengine-api-simulation/pyproject.toml +++ b/projects/policyengine-api-simulation/pyproject.toml @@ -29,6 +29,35 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = ["src/policyengine_api_simulation", "src/policyengine_api"] +# Modal image dependencies. Resolved inside uv.lock together with the +# project, then exported to requirements/modal-*.txt (see +# scripts/export-modal-image-requirements.sh); the images install those +# pinned exports so their packages can only change through a relock and +# always match the versions the test environment runs against. +[dependency-groups] +modal-simulation-image = [ + "uv", + "fastapi>=0.115.0", + "tables>=3.10.2", + "logfire>=3.0.0", + # logfire imports importlib_metadata unconditionally on Python 3.13 + # but does not declare it (see issue #602). + "importlib-metadata>=8", + "policyengine-observability[fastapi]>=1.3.0,<2", +] +modal-gateway-image = [ + "fastapi>=0.115.0", + "pydantic>=2.0", + # PyJWT powers the bearer-token decoder in gateway.auth. + "pyjwt>=2.10.1,<3.0.0", + # JWTDecoder lives in the policyengine-fastapi lib; it only needs + # the auth module at runtime here. + "cryptography>=41.0.0", + "logfire>=3.0.0", + "importlib-metadata>=8", + "policyengine-observability[fastapi]>=1.3.0,<2", +] + [tool.uv.sources] policyengine-fastapi = { path = "../../libs/policyengine-fastapi", editable = true } diff --git a/projects/policyengine-api-simulation/requirements/modal-gateway-image.txt b/projects/policyengine-api-simulation/requirements/modal-gateway-image.txt new file mode 100644 index 000000000..5fcbeb7bc --- /dev/null +++ b/projects/policyengine-api-simulation/requirements/modal-gateway-image.txt @@ -0,0 +1,47 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group modal-gateway-image --frozen --no-hashes --no-annotate --output-file requirements/modal-gateway-image.txt +annotated-types==0.7.0 +anyio==4.13.0 +asgiref==3.11.1 +certifi==2026.4.22 +cffi==2.0.0 ; platform_python_implementation != 'PyPy' +charset-normalizer==3.4.7 +cryptography==46.0.7 +deprecated==1.3.1 +executing==2.2.1 +fastapi==0.115.14 +googleapis-common-protos==1.74.0 +grpcio==1.80.0 +idna==3.13 +importlib-metadata==8.5.0 +logfire==4.6.0 +markdown-it-py==4.0.0 +mdurl==0.1.2 +opentelemetry-api==1.30.0 +opentelemetry-exporter-otlp-proto-common==1.30.0 +opentelemetry-exporter-otlp-proto-grpc==1.30.0 +opentelemetry-exporter-otlp-proto-http==1.30.0 +opentelemetry-instrumentation==0.51b0 +opentelemetry-instrumentation-asgi==0.51b0 +opentelemetry-instrumentation-fastapi==0.51b0 +opentelemetry-instrumentation-httpx==0.51b0 +opentelemetry-proto==1.30.0 +opentelemetry-sdk==1.30.0 +opentelemetry-semantic-conventions==0.51b0 +opentelemetry-util-http==0.51b0 +packaging==26.1 +policyengine-observability==1.3.0 +protobuf==5.29.6 +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' +pydantic==2.13.3 +pydantic-core==2.46.3 +pygments==2.20.0 +pyjwt==2.12.1 +requests==2.33.1 +rich==15.0.0 +starlette==0.46.2 +typing-extensions==4.15.0 +typing-inspection==0.4.2 +urllib3==2.6.3 +wrapt==1.17.3 +zipp==3.23.1 diff --git a/projects/policyengine-api-simulation/requirements/modal-simulation-image.txt b/projects/policyengine-api-simulation/requirements/modal-simulation-image.txt new file mode 100644 index 000000000..03db351f7 --- /dev/null +++ b/projects/policyengine-api-simulation/requirements/modal-simulation-image.txt @@ -0,0 +1,51 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group modal-simulation-image --frozen --no-hashes --no-annotate --output-file requirements/modal-simulation-image.txt +annotated-types==0.7.0 +anyio==4.13.0 +asgiref==3.11.1 +blosc2==4.1.2 +certifi==2026.4.22 +charset-normalizer==3.4.7 +deprecated==1.3.1 +executing==2.2.1 +fastapi==0.115.14 +googleapis-common-protos==1.74.0 +grpcio==1.80.0 +idna==3.13 +importlib-metadata==8.5.0 +logfire==4.6.0 +markdown-it-py==4.0.0 +mdurl==0.1.2 +msgpack==1.1.2 +ndindex==1.10.1 +numexpr==2.14.1 +numpy==2.4.4 +opentelemetry-api==1.30.0 +opentelemetry-exporter-otlp-proto-common==1.30.0 +opentelemetry-exporter-otlp-proto-grpc==1.30.0 +opentelemetry-exporter-otlp-proto-http==1.30.0 +opentelemetry-instrumentation==0.51b0 +opentelemetry-instrumentation-asgi==0.51b0 +opentelemetry-instrumentation-fastapi==0.51b0 +opentelemetry-instrumentation-httpx==0.51b0 +opentelemetry-proto==1.30.0 +opentelemetry-sdk==1.30.0 +opentelemetry-semantic-conventions==0.51b0 +opentelemetry-util-http==0.51b0 +packaging==26.1 +policyengine-observability==1.3.0 +protobuf==5.29.6 +py-cpuinfo==9.0.0 +pydantic==2.13.3 +pydantic-core==2.46.3 +pygments==2.20.0 +requests==2.33.1 +rich==15.0.0 +starlette==0.46.2 +tables==3.11.1 +typing-extensions==4.15.0 +typing-inspection==0.4.2 +urllib3==2.6.3 +uv==0.11.26 +wrapt==1.17.3 +zipp==3.23.1 diff --git a/projects/policyengine-api-simulation/src/modal/app.py b/projects/policyengine-api-simulation/src/modal/app.py index e2729c13a..57a04fafc 100644 --- a/projects/policyengine-api-simulation/src/modal/app.py +++ b/projects/policyengine-api-simulation/src/modal/app.py @@ -9,6 +9,7 @@ import modal import os +from pathlib import Path from policyengine_observability import operation, set_attribute @@ -139,16 +140,17 @@ def build_base_simulation_image() -> modal.Image: """ return ( modal.Image.debian_slim(python_version="3.13") - .pip_install( - "uv", - "fastapi>=0.115.0", - "tables>=3.10.2", - "logfire>=3.0.0", - # logfire imports importlib_metadata unconditionally but does - # not declare it as a dependency on Python 3.13, so install it - # explicitly or workers crash on ``import logfire``. - "importlib-metadata>=8", - "policyengine-observability[fastapi]>=1.3.0,<2", + # Pinned export of the modal-simulation-image dependency group in + # uv.lock, so image packages match the tested environment and can + # only change through a relock. Regenerate with + # scripts/export-modal-image-requirements.sh after editing the + # group or relocking. + .pip_install_from_requirements( + str( + Path(__file__).resolve().parents[2] + / "requirements" + / "modal-simulation-image.txt" + ) ) .run_commands( bundle_install_command(POLICYENGINE_VERSION), diff --git a/projects/policyengine-api-simulation/src/modal/gateway/app.py b/projects/policyengine-api-simulation/src/modal/gateway/app.py index 64144453e..b6f3a7044 100644 --- a/projects/policyengine-api-simulation/src/modal/gateway/app.py +++ b/projects/policyengine-api-simulation/src/modal/gateway/app.py @@ -9,6 +9,7 @@ """ import modal +from pathlib import Path from src.modal.logfire_legacy import configure_logfire @@ -20,20 +21,17 @@ # Lightweight image for gateway - no heavy dependencies gateway_image = ( modal.Image.debian_slim(python_version="3.13") - .pip_install( - "fastapi>=0.115.0", - "pydantic>=2.0", - # PyJWT powers the bearer-token decoder in gateway.auth. - "pyjwt>=2.10.1,<3.0.0", - # JWTDecoder lives in the policyengine-fastapi lib; it only needs - # the auth module at runtime here. - "cryptography>=41.0.0", - "logfire>=3.0.0", - # logfire imports importlib_metadata unconditionally but does not - # declare it as a dependency on Python 3.13, so install it - # explicitly or the container crashes at startup. - "importlib-metadata>=8", - "policyengine-observability[fastapi]>=1.3.0,<2", + # Pinned export of the modal-gateway-image dependency group in + # uv.lock, so image packages match the tested environment and can + # only change through a relock. Regenerate with + # scripts/export-modal-image-requirements.sh after editing the group + # or relocking. + .pip_install_from_requirements( + str( + Path(__file__).resolve().parents[3] + / "requirements" + / "modal-gateway-image.txt" + ) ) .add_local_python_source( "src.modal", diff --git a/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py b/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py index 1bb8c7596..93bca5d72 100644 --- a/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py +++ b/projects/policyengine-api-simulation/tests/test_modal_bundle_image.py @@ -1,8 +1,18 @@ import importlib import sys +from pathlib import Path from types import ModuleType +def requirements_package_names(path): + """Package names pinned in an exported requirements file.""" + return { + line.split(";")[0].split("==")[0].strip() + for line in Path(path).read_text().splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + + class FakeImage: def __init__(self): self.calls = [] @@ -17,6 +27,12 @@ def pip_install(self, *packages): self.calls.append(("pip_install", packages)) return self + def pip_install_from_requirements(self, requirements_txt, **kwargs): + self.calls.append( + ("pip_install_from_requirements", requirements_txt, kwargs) + ) + return self + def run_commands(self, *commands, **kwargs): self.calls.append(("run_commands", commands, kwargs)) return self @@ -88,17 +104,23 @@ def test_modal_image_uses_policyengine_bundle_install(monkeypatch): "/.policyengine-bundle-receipt.json" ) assert command_calls[0][2]["secrets"] == [app.data_secret, app.hf_secret] - pip_install_calls = [ - call for call in app.simulation_image.calls if call[0] == "pip_install" + requirements_calls = [ + call + for call in app.simulation_image.calls + if call[0] == "pip_install_from_requirements" ] - assert pip_install_calls - packages = pip_install_calls[0][1] - assert "policyengine-observability[fastapi]>=1.3.0,<2" in packages - assert "logfire>=3.0.0" in packages + assert requirements_calls + requirements_path = Path(requirements_calls[0][1]) + assert requirements_path.name == "modal-simulation-image.txt" + packages = requirements_package_names(requirements_path) + assert "policyengine-observability" in packages + assert "logfire" in packages # logfire needs importlib_metadata at import time on Python 3.13 but - # does not declare it; without the explicit install every worker - # crashes on ``import logfire``. - assert "importlib-metadata>=8" in packages + # does not declare it; the pinned export must keep providing it or + # every worker crashes on ``import logfire``. + assert "importlib-metadata" in packages + # uvx drives the policyengine bundle install into the image. + assert "uv" in packages runtime_secret_sets = { name: kwargs["secrets"] for name, kwargs in app.app.function_calls @@ -161,16 +183,22 @@ def test_gateway_image_installs_dual_observability(monkeypatch): app = importlib.import_module("src.modal.gateway.app") - pip_install_calls = [ - call for call in app.gateway_image.calls if call[0] == "pip_install" + requirements_calls = [ + call + for call in app.gateway_image.calls + if call[0] == "pip_install_from_requirements" ] - assert pip_install_calls - packages = pip_install_calls[0][1] - assert "policyengine-observability[fastapi]>=1.3.0,<2" in packages - assert "logfire>=3.0.0" in packages + assert requirements_calls + requirements_path = Path(requirements_calls[0][1]) + assert requirements_path.name == "modal-gateway-image.txt" + packages = requirements_package_names(requirements_path) + assert "policyengine-observability" in packages + assert "logfire" in packages # Same importlib_metadata gap as the simulation image: without this the # gateway ASGI factory dies in configure_logfire and every request 303s. - assert "importlib-metadata>=8" in packages + assert "importlib-metadata" in packages + assert "pyjwt" in packages + assert "cryptography" in packages function_kwargs = {name: kwargs for name, kwargs in app.app.function_calls} assert function_kwargs["web_app"]["secrets"] == [ diff --git a/projects/policyengine-api-simulation/tests/test_modal_image_requirements.py b/projects/policyengine-api-simulation/tests/test_modal_image_requirements.py new file mode 100644 index 000000000..102feb11e --- /dev/null +++ b/projects/policyengine-api-simulation/tests/test_modal_image_requirements.py @@ -0,0 +1,69 @@ +"""The Modal images install pinned exports of uv.lock dependency groups. + +These tests fail when the checked-in requirements files drift from +uv.lock — rerun scripts/export-modal-image-requirements.sh (make update +does it automatically). They also guard the regression from issue #602: +the exports must keep pinning logfire's undeclared importlib_metadata +runtime dependency. +""" + +import subprocess +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +GROUPS = ("modal-simulation-image", "modal-gateway-image") + + +def package_lines(text): + return [ + line.strip() + for line in text.splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + + +@pytest.mark.parametrize("group", GROUPS) +def test_checked_in_export_matches_lock(group): + checked_in = PROJECT_ROOT / "requirements" / f"{group}.txt" + assert checked_in.exists(), ( + f"{checked_in} is missing; run scripts/export-modal-image-requirements.sh" + ) + exported = subprocess.run( + [ + "uv", + "export", + "--only-group", + group, + "--frozen", + "--no-hashes", + "--no-annotate", + ], + cwd=PROJECT_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + assert package_lines(checked_in.read_text()) == package_lines(exported), ( + f"requirements/{group}.txt is stale relative to uv.lock; " + "run scripts/export-modal-image-requirements.sh" + ) + + +@pytest.mark.parametrize("group", GROUPS) +def test_export_is_fully_pinned(group): + checked_in = PROJECT_ROOT / "requirements" / f"{group}.txt" + for line in package_lines(checked_in.read_text()): + requirement = line.split(";")[0].strip() + assert "==" in requirement, f"unpinned requirement in {group}: {line}" + + +@pytest.mark.parametrize("group", GROUPS) +def test_export_pins_logfire_runtime_deps(group): + checked_in = PROJECT_ROOT / "requirements" / f"{group}.txt" + names = { + line.split(";")[0].split("==")[0].strip() + for line in package_lines(checked_in.read_text()) + } + assert {"logfire", "importlib-metadata"} <= names diff --git a/projects/policyengine-api-simulation/uv.lock b/projects/policyengine-api-simulation/uv.lock index fde93d9c1..ae9d1c538 100644 --- a/projects/policyengine-api-simulation/uv.lock +++ b/projects/policyengine-api-simulation/uv.lock @@ -1847,6 +1847,25 @@ test = [ { name = "pytest-cov" }, ] +[package.dev-dependencies] +modal-gateway-image = [ + { name = "cryptography" }, + { name = "fastapi" }, + { name = "importlib-metadata" }, + { name = "logfire" }, + { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "pydantic" }, + { name = "pyjwt" }, +] +modal-simulation-image = [ + { name = "fastapi" }, + { name = "importlib-metadata" }, + { name = "logfire" }, + { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "tables" }, + { name = "uv" }, +] + [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, @@ -1870,6 +1889,25 @@ requires-dist = [ ] provides-extras = ["test", "build"] +[package.metadata.requires-dev] +modal-gateway-image = [ + { name = "cryptography", specifier = ">=41.0.0" }, + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "importlib-metadata", specifier = ">=8" }, + { name = "logfire", specifier = ">=3.0.0" }, + { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, +] +modal-simulation-image = [ + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "importlib-metadata", specifier = ">=8" }, + { name = "logfire", specifier = ">=3.0.0" }, + { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "tables", specifier = ">=3.10.2" }, + { name = "uv" }, +] + [[package]] name = "policyengine-uk" version = "2.89.2" @@ -2704,6 +2742,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/a8/1791660a87f03d10a3bce00401a66035999c91f5a9a6987569b84df5719d/us-3.2.0-py3-none-any.whl", hash = "sha256:571714ad6d473c72bbd2058a53404cdf4ecc0129e4f19adfcbeb4e2d7e3dc3e7", size = 13775, upload-time = "2024-07-22T01:09:41.432Z" }, ] +[[package]] +name = "uv" +version = "0.11.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/cb/5efc713948ddb10b00abfb51bfd429221c720175557f9c7965fea2448fe4/uv-0.11.26.tar.gz", hash = "sha256:2a433ece2ace088dd572d8abb0e6bd9a4ecb0e10bc9856447bbb37545f384f29", size = 4331220, upload-time = "2026-06-30T14:52:03.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/71/86dbffac9e26df28a16639c426cf4ba572aaf43d9231463e0dca337895b2/uv-0.11.26-py3-none-linux_armv6l.whl", hash = "sha256:fb97bf04512dfe16d86084e75d8129701fc8da9fb40de8746b73c3aa617c5897", size = 25197324, upload-time = "2026-06-30T14:50:51.75Z" }, + { url = "https://files.pythonhosted.org/packages/ec/80/525b73c8188e7052343e7109466a08fcd5195055aff4b0346ce3622e48cb/uv-0.11.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a58a06e5a4b0035538d3ab4160ad74c716076ea7148eb3317171c6276ac020b4", size = 24179172, upload-time = "2026-06-30T14:50:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/cf7b94ed3b1932c2a62573dcd388ad6c1da5c52111cd71ab7f20faa4a0aa/uv-0.11.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b6d078d2ce83897884c2330c0676f27be4bf3d223fb2a409460f579fb5f0a98", size = 22949576, upload-time = "2026-06-30T14:51:00.538Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fd/71fa021f6909c4139d8354bea623b5e0ef0ce4a08da250da1a1645528da2/uv-0.11.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1cd9ba4951681ce17f1703106266fcbe27aaa7d37f07d53cce8b5686d68a8755", size = 24936673, upload-time = "2026-06-30T14:51:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/273425e58a8812423e3d1f6c5da1015e636fbf13a83d104317ca37e16304/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e4f4c3268e69ac96f01972274a62f5f930c03cbc680adba6f21e63237ba3a639", size = 24719617, upload-time = "2026-06-30T14:51:08.419Z" }, + { url = "https://files.pythonhosted.org/packages/81/f8/1601e2acc7c54963814b4831eab996d8599e690712722c5acec5114860be/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:efcbe0e187846f5ddba23bcaed17e4f9cd2463da5c45bdb5869616f686d713ff", size = 24734176, upload-time = "2026-06-30T14:51:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/88/d2/a8a422e54c08cf4b8d51bedb9dbdd3cc233aa290ad8b3ee0438c0c02a3a5/uv-0.11.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:120ab2de93164d08cf5950f7fe18cbebe3ff670865ae41a292452bab2346477f", size = 26158780, upload-time = "2026-06-30T14:51:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/db/e6/647fe5fdc888a3d27f79977877ce4e88052fe9be5398371e51bb134fc262/uv-0.11.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9052bf27c7ee426901f35a48715fa9288ce631c1878b91c9a6c950288f4b8633", size = 27009550, upload-time = "2026-06-30T14:51:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/72/c2/85d8e762ad83b0f14fae2255b0578c4fd7dc915746f81b64ed786342627a/uv-0.11.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efdddfcc9b1b790c5f7985c5c183c851682ced165b44ffa914f4947f5cad1fbf", size = 26183777, upload-time = "2026-06-30T14:51:24.715Z" }, + { url = "https://files.pythonhosted.org/packages/d3/00/478c3a870dcac690b8c337ee950a60a952e817f574945e85155c3cc0ab34/uv-0.11.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dcf4e0b5b5cbdc242dcb002f1f8d99e7cf8c043609869228a9ce15e095c0b18", size = 26260589, upload-time = "2026-06-30T14:51:28.809Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/e4e43e106fb8cdc026b97491ea4600f4194a9c4da0b4e4e30c2a7dceb268/uv-0.11.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866ae8d28f7381c15de0906a284c1e97916424c635bf40f7960b3fc889cd725e", size = 25073850, upload-time = "2026-06-30T14:51:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c2/e772b7e6c8a835e8bf6739a391cdfc8e8e244c5c496d9b40625068b59ff4/uv-0.11.26-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:22f6d62e794b252ff3a1e2dfe5010cc76208f90b2c906e54971a0223ad6f16bc", size = 25682609, upload-time = "2026-06-30T14:51:36.888Z" }, + { url = "https://files.pythonhosted.org/packages/1a/69/ea77209a224a23a399cb7f6414f77ef032bd9e083e01199a0ebebf0d3ff2/uv-0.11.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:edd0c12b75141a6d830d138a91e366ad66e630f1c1dcaf83b8325b80cbacfcbb", size = 25800556, upload-time = "2026-06-30T14:51:40.937Z" }, + { url = "https://files.pythonhosted.org/packages/77/60/b6c0c03d2538a016b6624fa251960012e564ea02f841e958c7d60e974685/uv-0.11.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:af6a45b11a569cc4d2437e89a25a53dcf753f2a02a8f2de96be09b9b942cb3ec", size = 25385658, upload-time = "2026-06-30T14:51:45.103Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e7/46881ff9164aa2e7c649901837d58eee3c57beb3b0fcc0fea6a4e40cf8f3/uv-0.11.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c28822517d03aebbe9549aaaecc88ad580e4b2b6a927abffe5774a74d6ba09f6", size = 26551013, upload-time = "2026-06-30T14:51:49.062Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/380dad6c2bbe12417025aacd12cfc08322ed4c9dd8f760bff7035b86f22d/uv-0.11.26-py3-none-win32.whl", hash = "sha256:79e5c1b3410047e1962290c3b7b8f512d2c1bb95200c60b016f7729287cf34c0", size = 23947180, upload-time = "2026-06-30T14:51:53.065Z" }, + { url = "https://files.pythonhosted.org/packages/d0/13/9c588226d5b478328d739e654944430719f3ffe8999d6a24d425ec9664ab/uv-0.11.26-py3-none-win_amd64.whl", hash = "sha256:d95567e9470dc48ff03265f420c3c6973f6437f18a79d5e00b6eb4b2d9379907", size = 26909320, upload-time = "2026-06-30T14:51:57.235Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/ea66b12813878797126e2b3aca124b1c9c5ef53120702d1c00172f90a21d/uv-0.11.26-py3-none-win_arm64.whl", hash = "sha256:7e69d1569afbb936e7bf4e4ab2f72d606405f4a68f380f088a0b2233e84e056a", size = 25176820, upload-time = "2026-06-30T14:52:01.05Z" }, +] + [[package]] name = "uvicorn" version = "0.46.0" diff --git a/scripts/export-modal-image-requirements.sh b/scripts/export-modal-image-requirements.sh new file mode 100755 index 000000000..f807d062a --- /dev/null +++ b/scripts/export-modal-image-requirements.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Export the Modal image dependency groups from uv.lock to pinned +# requirements files the image definitions install from. Rerun after any +# relock; make update does this automatically, and a unit test +# (tests/test_modal_image_requirements.py) fails CI if the exports drift +# from the lock. +set -euo pipefail + +cd "$(dirname "$0")/../projects/policyengine-api-simulation" +mkdir -p requirements + +for group in modal-simulation-image modal-gateway-image; do + uv export \ + --only-group "$group" \ + --frozen \ + --no-hashes \ + --no-annotate \ + --output-file "requirements/$group.txt" \ + --quiet +done