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 b57666745..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,12 +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", - "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 e2a48825d..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,16 +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", - "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 a41227f68..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,13 +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; 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 @@ -157,13 +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" 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