From 859fb89404837c90bf210d3e1758f116a7b9f552 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Wed, 20 May 2026 18:06:53 +0200 Subject: [PATCH 1/3] Fix Modal gateway routing metadata imports --- changelog.d/1543.fixed.md | 1 + .../decorators/analytics.py | 2 +- .../modal_release/gateway.py | 2 +- .../routing_metadata.py} | 7 +-- .../modal_release/worker_dispatch.py | 2 +- tests/unit/endpoints/test_household.py | 2 +- tests/unit/modal_release/test_gateway.py | 2 +- .../modal_release/test_gateway_imports.py | 56 +++++++++++++++++++ .../modal_release/test_worker_dispatch.py | 2 +- 9 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 changelog.d/1543.fixed.md rename policyengine_household_api/{utils/modal_routing_metadata.py => modal_release/routing_metadata.py} (85%) create mode 100644 tests/unit/modal_release/test_gateway_imports.py diff --git a/changelog.d/1543.fixed.md b/changelog.d/1543.fixed.md new file mode 100644 index 00000000..ab3b1625 --- /dev/null +++ b/changelog.d/1543.fixed.md @@ -0,0 +1 @@ +Fixed Modal gateway imports so routing metadata does not pull worker-only dependencies into the lightweight gateway image. diff --git a/policyengine_household_api/decorators/analytics.py b/policyengine_household_api/decorators/analytics.py index 7d19c017..49e9783c 100644 --- a/policyengine_household_api/decorators/analytics.py +++ b/policyengine_household_api/decorators/analytics.py @@ -25,7 +25,7 @@ ModalResolvedChannel, VariableUsageSummary, ) -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( REQUESTED_VERSION_ENVIRON_KEY, RESOLVED_CHANNEL_ENVIRON_KEY, ) diff --git a/policyengine_household_api/modal_release/gateway.py b/policyengine_household_api/modal_release/gateway.py index 6361ef00..2a911e77 100644 --- a/policyengine_household_api/modal_release/gateway.py +++ b/policyengine_household_api/modal_release/gateway.py @@ -13,7 +13,7 @@ empty_manifest, validate_manifest, ) -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( MODAL_ROUTING_PAYLOAD_KEY, modal_routing_payload, ) diff --git a/policyengine_household_api/utils/modal_routing_metadata.py b/policyengine_household_api/modal_release/routing_metadata.py similarity index 85% rename from policyengine_household_api/utils/modal_routing_metadata.py rename to policyengine_household_api/modal_release/routing_metadata.py index ac9123c7..2ac9381b 100644 --- a/policyengine_household_api/utils/modal_routing_metadata.py +++ b/policyengine_household_api/modal_release/routing_metadata.py @@ -2,12 +2,11 @@ from typing import Any -from policyengine_household_api.models.analytics import ModalResolvedChannel - MODAL_ROUTING_PAYLOAD_KEY = "modal_routing" REQUESTED_VERSION_ENVIRON_KEY = "policyengine.requested_version" RESOLVED_CHANNEL_ENVIRON_KEY = "policyengine.resolved_channel" +RESOLVED_CHANNEL_VALUES = {"current", "frontier"} def modal_routing_payload( @@ -30,9 +29,7 @@ def routing_environ_overrides(payload: dict[str, Any]) -> dict[str, str]: resolved_channel = routing.get("resolved_channel") if not isinstance(requested_version, str) or not requested_version: return {} - if resolved_channel not in { - channel.value for channel in ModalResolvedChannel - }: + if resolved_channel not in RESOLVED_CHANNEL_VALUES: return {} return { diff --git a/policyengine_household_api/modal_release/worker_dispatch.py b/policyengine_household_api/modal_release/worker_dispatch.py index e6c3e4d3..f5042f87 100644 --- a/policyengine_household_api/modal_release/worker_dispatch.py +++ b/policyengine_household_api/modal_release/worker_dispatch.py @@ -2,7 +2,7 @@ from typing import Any -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( routing_environ_overrides, ) diff --git a/tests/unit/endpoints/test_household.py b/tests/unit/endpoints/test_household.py index b8b5e602..089187cc 100644 --- a/tests/unit/endpoints/test_household.py +++ b/tests/unit/endpoints/test_household.py @@ -3,7 +3,7 @@ import pytest from policyengine_household_api.constants import COUNTRY_PACKAGE_VERSIONS from policyengine_household_api.endpoints.household import _validate_axes -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( REQUESTED_VERSION_ENVIRON_KEY, RESOLVED_CHANNEL_ENVIRON_KEY, ) diff --git a/tests/unit/modal_release/test_gateway.py b/tests/unit/modal_release/test_gateway.py index 9298afef..5ee34767 100644 --- a/tests/unit/modal_release/test_gateway.py +++ b/tests/unit/modal_release/test_gateway.py @@ -11,7 +11,7 @@ from policyengine_household_api.modal_release.manifest import ( MANIFEST_SCHEMA_VERSION, ) -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( MODAL_ROUTING_PAYLOAD_KEY, ) diff --git a/tests/unit/modal_release/test_gateway_imports.py b/tests/unit/modal_release/test_gateway_imports.py new file mode 100644 index 00000000..3ff7473e --- /dev/null +++ b/tests/unit/modal_release/test_gateway_imports.py @@ -0,0 +1,56 @@ +import importlib +from importlib.abc import MetaPathFinder +import sys + + +class _ForbiddenImportGuard(MetaPathFinder): + def __init__(self, blocked_roots: set[str]): + self.blocked_roots = blocked_roots + + def find_spec(self, fullname, path=None, target=None): + if any( + fullname == root or fullname.startswith(f"{root}.") + for root in self.blocked_roots + ): + raise AssertionError( + f"Gateway import unexpectedly imported {fullname}" + ) + return None + + +def test_gateway_import_keeps_gateway_image_dependency_boundary(): + _remove_modules( + { + "policyengine_household_api.modal_release.gateway", + "policyengine_household_api.modal_release.routing_metadata", + "policyengine_household_api.models", + "policyengine_household_api.utils", + "numpy", + "pydantic", + } + ) + guard = _ForbiddenImportGuard( + { + "policyengine_household_api.models", + "policyengine_household_api.utils", + "numpy", + "pydantic", + } + ) + + sys.meta_path.insert(0, guard) + try: + importlib.import_module( + "policyengine_household_api.modal_release.gateway" + ) + finally: + sys.meta_path.remove(guard) + + +def _remove_modules(module_roots: set[str]) -> None: + for module_name in list(sys.modules): + if any( + module_name == root or module_name.startswith(f"{root}.") + for root in module_roots + ): + sys.modules.pop(module_name, None) diff --git a/tests/unit/modal_release/test_worker_dispatch.py b/tests/unit/modal_release/test_worker_dispatch.py index 8898b05d..87b9fcaa 100644 --- a/tests/unit/modal_release/test_worker_dispatch.py +++ b/tests/unit/modal_release/test_worker_dispatch.py @@ -3,7 +3,7 @@ from policyengine_household_api.modal_release.worker_dispatch import ( dispatch_to_flask_app, ) -from policyengine_household_api.utils.modal_routing_metadata import ( +from policyengine_household_api.modal_release.routing_metadata import ( MODAL_ROUTING_PAYLOAD_KEY, REQUESTED_VERSION_ENVIRON_KEY, RESOLVED_CHANNEL_ENVIRON_KEY, From fea894b4d2e588a1ab7e084499ceaefae53147c4 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Wed, 20 May 2026 18:12:54 +0200 Subject: [PATCH 2/3] Document Modal gateway channel literals --- policyengine_household_api/modal_release/routing_metadata.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_household_api/modal_release/routing_metadata.py b/policyengine_household_api/modal_release/routing_metadata.py index 2ac9381b..a11aa2d9 100644 --- a/policyengine_household_api/modal_release/routing_metadata.py +++ b/policyengine_household_api/modal_release/routing_metadata.py @@ -6,6 +6,9 @@ MODAL_ROUTING_PAYLOAD_KEY = "modal_routing" REQUESTED_VERSION_ENVIRON_KEY = "policyengine.requested_version" RESOLVED_CHANNEL_ENVIRON_KEY = "policyengine.resolved_channel" +# Keep this literal in sync with the app's channel values manually. Importing +# the shared enum here pulls heavy, unnecessary dependencies into the Modal +# gateway image and has broken production gateway startup. RESOLVED_CHANNEL_VALUES = {"current", "frontier"} From ea712edad5207fcdf35d9d58d5d5b79a2c1e82f3 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Wed, 20 May 2026 18:35:51 +0200 Subject: [PATCH 3/3] Remove gateway import isolation test --- .../modal_release/test_gateway_imports.py | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 tests/unit/modal_release/test_gateway_imports.py diff --git a/tests/unit/modal_release/test_gateway_imports.py b/tests/unit/modal_release/test_gateway_imports.py deleted file mode 100644 index 3ff7473e..00000000 --- a/tests/unit/modal_release/test_gateway_imports.py +++ /dev/null @@ -1,56 +0,0 @@ -import importlib -from importlib.abc import MetaPathFinder -import sys - - -class _ForbiddenImportGuard(MetaPathFinder): - def __init__(self, blocked_roots: set[str]): - self.blocked_roots = blocked_roots - - def find_spec(self, fullname, path=None, target=None): - if any( - fullname == root or fullname.startswith(f"{root}.") - for root in self.blocked_roots - ): - raise AssertionError( - f"Gateway import unexpectedly imported {fullname}" - ) - return None - - -def test_gateway_import_keeps_gateway_image_dependency_boundary(): - _remove_modules( - { - "policyengine_household_api.modal_release.gateway", - "policyengine_household_api.modal_release.routing_metadata", - "policyengine_household_api.models", - "policyengine_household_api.utils", - "numpy", - "pydantic", - } - ) - guard = _ForbiddenImportGuard( - { - "policyengine_household_api.models", - "policyengine_household_api.utils", - "numpy", - "pydantic", - } - ) - - sys.meta_path.insert(0, guard) - try: - importlib.import_module( - "policyengine_household_api.modal_release.gateway" - ) - finally: - sys.meta_path.remove(guard) - - -def _remove_modules(module_roots: set[str]) -> None: - for module_name in list(sys.modules): - if any( - module_name == root or module_name.startswith(f"{root}.") - for root in module_roots - ): - sys.modules.pop(module_name, None)