diff --git a/.github/find-api-model-versions.py b/.github/find-api-model-versions.py index 0c0f53fcc..7a9a9f09b 100644 --- a/.github/find-api-model-versions.py +++ b/.github/find-api-model-versions.py @@ -1,13 +1,18 @@ +import argparse import os import sys -from policyengine_api.constants import COUNTRY_PACKAGE_VERSIONS +from policyengine_api.constants import ( + COUNTRY_PACKAGE_VERSIONS, + POLICYENGINE_CORE_VERSION, + POLICYENGINE_VERSION, +) -def find_api_model_versions_and_output_to_github(): + +def find_api_model_versions() -> dict[str, str]: """ - Find the API model versions and output them to a file for GitHub. + Find the API model versions from the installed PolicyEngine bundle. """ - # Try to get package versions for US and UK us_version = COUNTRY_PACKAGE_VERSIONS.get("us") uk_version = COUNTRY_PACKAGE_VERSIONS.get("uk") @@ -19,13 +24,41 @@ def find_api_model_versions_and_output_to_github(): print("Error: UK package version not found.", file=sys.stderr) sys.exit(1) - # Write to GitHub Actions environment + if not POLICYENGINE_VERSION: + print("Error: PolicyEngine package version not found.", file=sys.stderr) + sys.exit(1) + + return { + "POLICYENGINE_VERSION": POLICYENGINE_VERSION, + "POLICYENGINE_CORE_VERSION": POLICYENGINE_CORE_VERSION, + "US_VERSION": us_version, + "UK_VERSION": uk_version, + } + + +def find_api_model_versions_and_output_to_github(): + """ + Find the API model versions and output them to a file for GitHub. + """ + versions = find_api_model_versions() with open(os.environ["GITHUB_ENV"], "a") as f: - f.write(f"US_VERSION={us_version}\n") - f.write(f"UK_VERSION={uk_version}\n") + for key, value in versions.items(): + f.write(f"{key}={value}\n") if __name__ == "__main__": - find_api_model_versions_and_output_to_github() - print("API model versions found and written to GitHub environment.") + parser = argparse.ArgumentParser() + parser.add_argument( + "--shell", + action="store_true", + help="Print shell-compatible KEY=VALUE lines instead of writing GITHUB_ENV.", + ) + args = parser.parse_args() + + if args.shell: + for key, value in find_api_model_versions().items(): + print(f"{key}={value}") + else: + find_api_model_versions_and_output_to_github() + print("API model versions found and written to GitHub environment.") sys.exit(0) diff --git a/.github/request-simulation-model-versions.sh b/.github/request-simulation-model-versions.sh index 0be50e8f8..1bf77b142 100755 --- a/.github/request-simulation-model-versions.sh +++ b/.github/request-simulation-model-versions.sh @@ -1,39 +1,43 @@ #!/usr/bin/env bash -set -e - -# Modal Gateway version check script -# Verifies that the US package version used by API v1 is deployed -# in the Modal simulation API before allowing API v1 deployment to proceed. -# -# NOTE: We explicitly do NOT check for UK versions here. The UK package -# (policyengine-uk) does not support the older Python versions that API v1 -# runs on, so the UK version deployed to Modal may not match the version -# pinned in API v1's requirements. -# -# Usage: ./request-simulation-model-versions.sh -us +set -euo pipefail + +# Modal gateway version check script. +# Verifies that the PolicyEngine .py bundle used by API v1 is deployed in the +# simulation gateway before allowing API v1 deployment to proceed. US/UK +# versions are optional compatibility checks that should resolve to the same +# gateway app as the .py bundle route. GATEWAY_URL="${SIMULATION_API_URL:-https://policyengine--policyengine-simulation-gateway-web-app.modal.run}" usage() { - echo "Usage: $0 -us " + echo "Usage: $0 -py [-us ] [-uk ]" echo "" echo "Required flags:" - echo " -us us_version - US package version (e.g., 1.459.0)" + echo " -py, --policyengine policyengine_version PolicyEngine .py bundle version" + echo "" + echo "Optional compatibility checks:" + echo " -us us_version Expected bundled policyengine-us version" + echo " -uk uk_version Expected bundled policyengine-uk version" exit 1 } +POLICYENGINE_VERSION="" US_VERSION="" +UK_VERSION="" while [ $# -gt 0 ]; do case "$1" in + -py|--policyengine) + POLICYENGINE_VERSION="$2" + shift 2 + ;; -us) US_VERSION="$2" shift 2 ;; -uk) - # Accept but ignore UK version flag for backwards compatibility - echo "Note: UK version check is disabled (see script comments)" + UK_VERSION="$2" shift 2 ;; -h|--help) @@ -46,17 +50,22 @@ while [ $# -gt 0 ]; do esac done -if [ -z "$US_VERSION" ]; then - echo "Error: -us version is required" +if [ -z "$POLICYENGINE_VERSION" ]; then + echo "Error: -py/--policyengine version is required" usage fi echo "Checking Modal simulation API versions..." echo " Gateway: $GATEWAY_URL" -echo " Expected US version: $US_VERSION" +echo " Expected PolicyEngine .py bundle: $POLICYENGINE_VERSION" +if [ -n "$US_VERSION" ]; then + echo " Expected policyengine-us: $US_VERSION" +fi +if [ -n "$UK_VERSION" ]; then + echo " Expected policyengine-uk: $UK_VERSION" +fi echo "" -# Query the gateway for deployed versions VERSIONS_RESPONSE=$(curl -s "${GATEWAY_URL}/versions") if [ -z "$VERSIONS_RESPONSE" ]; then @@ -64,16 +73,43 @@ if [ -z "$VERSIONS_RESPONSE" ]; then exit 1 fi -# Check if US version is deployed -US_DEPLOYED=$(echo "$VERSIONS_RESPONSE" | jq -r --arg v "$US_VERSION" '.us[$v] // empty') -if [ -z "$US_DEPLOYED" ]; then - echo "ERROR: US version $US_VERSION is NOT deployed in Modal simulation API" - echo "Available US versions:" - echo "$VERSIONS_RESPONSE" | jq -r '.us | keys[]' +BUNDLE_APP=$(echo "$VERSIONS_RESPONSE" | jq -r --arg v "$POLICYENGINE_VERSION" '.policyengine[$v] // empty') +if [ -z "$BUNDLE_APP" ]; then + echo "ERROR: PolicyEngine .py bundle $POLICYENGINE_VERSION is NOT deployed in the simulation API" + echo "Available PolicyEngine versions:" + echo "$VERSIONS_RESPONSE" | jq -r '.policyengine | keys[]' exit 1 fi -echo "US version $US_VERSION is deployed (app: $US_DEPLOYED)" +echo "PolicyEngine .py bundle $POLICYENGINE_VERSION is deployed (app: $BUNDLE_APP)" + +check_country_route() { + local country="$1" + local version="$2" + local app + + if [ -z "$version" ]; then + return + fi + + app=$(echo "$VERSIONS_RESPONSE" | jq -r --arg country "$country" --arg v "$version" '.[$country][$v] // empty') + if [ -z "$app" ]; then + echo "ERROR: ${country} version ${version} is NOT deployed in the simulation API" + echo "Available ${country} versions:" + echo "$VERSIONS_RESPONSE" | jq -r --arg country "$country" '.[$country] | keys[]' + exit 1 + fi + + if [ "$app" != "$BUNDLE_APP" ]; then + echo "ERROR: ${country} version ${version} resolves to ${app}, not bundle app ${BUNDLE_APP}" + exit 1 + fi + + echo "${country} version ${version} resolves to the same app" +} + +check_country_route "us" "$US_VERSION" +check_country_route "uk" "$UK_VERSION" echo "" -echo "SUCCESS: US version is deployed and ready" +echo "SUCCESS: PolicyEngine bundle route is deployed and ready" exit 0 diff --git a/.github/scripts/update-package.sh b/.github/scripts/update-package.sh deleted file mode 100755 index 5800669e4..000000000 --- a/.github/scripts/update-package.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash -# -# Check if a country package has a newer version on PyPI than what is -# pinned in pyproject.toml. If so, update the pin, create a changelog -# fragment, and open a version-specific PR. -# -# Usage: ./update-package.sh -# e.g. ./update-package.sh policyengine-us -# -# Requires: curl, jq, sed, git, gh (GitHub CLI), python3 + requests -# Environment: GH_TOKEN must be set for the gh CLI. -set -euo pipefail - -PACKAGE="${1:?Usage: update-package.sh }" - -# Derive the underscore form used in pyproject.toml -# (policyengine-us -> policyengine_us) -PACKAGE_UNDERSCORE="${PACKAGE//-/_}" - -# Derive human-readable display name -# (policyengine-us -> PolicyEngine US) -COUNTRY="${PACKAGE#policyengine-}" -COUNTRY_UPPER=$(echo "$COUNTRY" | tr '[:lower:]' '[:upper:]') -DISPLAY_NAME="PolicyEngine ${COUNTRY_UPPER}" - -# --------------------------------------------------------------------------- -# 1. Read current pinned version from pyproject.toml -# --------------------------------------------------------------------------- -CURRENT=$(grep -oP "(?<=${PACKAGE_UNDERSCORE}==)[0-9]+\.[0-9]+\.[0-9]+" pyproject.toml || true) -if [[ -z "$CURRENT" ]]; then - echo "Package '${PACKAGE_UNDERSCORE}' not found in pyproject.toml. Skipping." - exit 0 -fi -echo "Current version: ${PACKAGE_UNDERSCORE}==${CURRENT}" - -# --------------------------------------------------------------------------- -# 2. Fetch latest version from PyPI -# --------------------------------------------------------------------------- -LATEST=$(curl -sf "https://pypi.org/pypi/${PACKAGE}/json" | jq -r .info.version) -if [[ -z "$LATEST" || "$LATEST" == "null" ]]; then - echo "Could not fetch latest version for '${PACKAGE}' from PyPI." - exit 1 -fi -echo "Latest version: ${PACKAGE}==${LATEST}" - -# --------------------------------------------------------------------------- -# 3. Compare — exit early if already up to date -# --------------------------------------------------------------------------- -if [[ "$CURRENT" == "$LATEST" ]]; then - echo "Already up to date. Nothing to do." - exit 0 -fi -echo "Update available: ${CURRENT} -> ${LATEST}" - -# --------------------------------------------------------------------------- -# 4. Check if a branch for this exact version already exists -# --------------------------------------------------------------------------- -BRANCH="auto/update-${PACKAGE}-${LATEST}" - -if git ls-remote --exit-code --heads origin "$BRANCH" &>/dev/null; then - echo "Branch '${BRANCH}' already exists on remote. Skipping." - exit 0 -fi - -# --------------------------------------------------------------------------- -# 5. Update version pin in pyproject.toml -# --------------------------------------------------------------------------- -sed -i "s/${PACKAGE_UNDERSCORE}==${CURRENT}/${PACKAGE_UNDERSCORE}==${LATEST}/" pyproject.toml - -if git diff --quiet pyproject.toml; then - echo "No changes to pyproject.toml after substitution. Skipping." - exit 0 -fi - -# --------------------------------------------------------------------------- -# 6. Create changelog fragment (required by PR CI) -# --------------------------------------------------------------------------- -FRAGMENT="changelog.d/update-${PACKAGE}-${LATEST}.changed.md" -echo "Update ${DISPLAY_NAME} to ${LATEST}." > "$FRAGMENT" - -# --------------------------------------------------------------------------- -# 7. Fetch upstream changelog for the PR body -# --------------------------------------------------------------------------- -CHANGELOG=$(python3 .github/scripts/check_updates.py \ - --package "$PACKAGE" \ - --old-version "$CURRENT" \ - --new-version "$LATEST" 2>/dev/null || echo "") - -PR_BODY="## Summary - -Update ${DISPLAY_NAME} from ${CURRENT} to ${LATEST}." - -if [[ -n "$CHANGELOG" ]]; then - PR_BODY="${PR_BODY} - -## What changed (${CURRENT} -> ${LATEST}) - -${CHANGELOG}" -fi - -PR_BODY="${PR_BODY} - ---- -Generated automatically by GitHub Actions" - -# --------------------------------------------------------------------------- -# 8. Commit, push (no force), and open PR -# --------------------------------------------------------------------------- -git config user.name "github-actions[bot]" -git config user.email "github-actions[bot]@users.noreply.github.com" - -git checkout -b "$BRANCH" -git add pyproject.toml "$FRAGMENT" -git commit -m "Update ${DISPLAY_NAME} to ${LATEST}" -git push -u origin "$BRANCH" - -gh pr create \ - --base master \ - --title "Update ${DISPLAY_NAME} to ${LATEST}" \ - --body "$PR_BODY" - -echo "PR created: ${DISPLAY_NAME} ${CURRENT} -> ${LATEST}" diff --git a/.github/scripts/update-policyengine-package.sh b/.github/scripts/update-policyengine-package.sh new file mode 100755 index 000000000..84c9326a2 --- /dev/null +++ b/.github/scripts/update-policyengine-package.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# Check whether PolicyEngine .py has a newer release on PyPI. If so, update the +# policyengine[models] pin, refresh uv.lock, derive bundled package versions, +# create a changelog fragment, and open a PR. +# +# Environment: +# GH_TOKEN must be set for gh. +# LATEST_OVERRIDE may be set for testing a specific version. +set -euo pipefail + +DRY_RUN=0 +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=1 +fi + +CURRENT=$(python3 -c ' +import re +from pathlib import Path + +match = re.search(r"policyengine\[models\]==([0-9]+\.[0-9]+\.[0-9]+)", Path("pyproject.toml").read_text()) +print(match.group(1) if match else "") +') +if [[ -z "$CURRENT" ]]; then + echo "policyengine[models] pin not found in pyproject.toml" + exit 1 +fi +echo "Current PolicyEngine bundle: ${CURRENT}" + +if [[ -n "${LATEST_OVERRIDE:-}" ]]; then + LATEST="$LATEST_OVERRIDE" +else + LATEST=$(python3 -c ' +import requests + +response = requests.get("https://pypi.org/pypi/policyengine/json", timeout=30) +response.raise_for_status() +print(response.json()["info"]["version"]) +') +fi +echo "Latest PolicyEngine bundle: ${LATEST}" + +if [[ "$CURRENT" == "$LATEST" ]]; then + echo "Already up to date. Nothing to do." + exit 0 +fi + +BRANCH="auto/update-policyengine-bundle-${LATEST}" +if git ls-remote --exit-code --heads origin "$BRANCH" &>/dev/null; then + echo "Branch '${BRANCH}' already exists on remote. Skipping." + exit 0 +fi + +if [[ "$DRY_RUN" == "1" ]]; then + echo "Dry run complete. Would update PolicyEngine .py bundle from ${CURRENT} to ${LATEST}." + echo "Would update pyproject.toml, refresh uv.lock, create a changelog fragment, and open branch '${BRANCH}'." + exit 0 +fi + +python3 -c ' +import re +import sys +from pathlib import Path + +current, latest = sys.argv[1], sys.argv[2] +path = Path("pyproject.toml") +text = path.read_text() +updated = re.sub( + rf"policyengine\[models\]=={re.escape(current)}", + f"policyengine[models]=={latest}", + text, + count=1, +) +if updated == text: + raise SystemExit("No policyengine[models] pin changed") +path.write_text(updated) +' "$CURRENT" "$LATEST" + +uv lock --upgrade-package policyengine + +VERSIONS_OUTPUT=$(uv run python .github/find-api-model-versions.py --shell) +eval "$VERSIONS_OUTPUT" + +FRAGMENT="changelog.d/update-policyengine-bundle-${LATEST}.changed.md" +echo "Update the PolicyEngine bundle to ${LATEST}." > "$FRAGMENT" + +PR_BODY="## Summary + +Update PolicyEngine .py bundle from ${CURRENT} to ${LATEST}. + +## Bundled versions + +- policyengine: ${POLICYENGINE_VERSION} +- policyengine-core: ${POLICYENGINE_CORE_VERSION} +- policyengine-us: ${US_VERSION} +- policyengine-uk: ${UK_VERSION} + +--- +Generated automatically by GitHub Actions" + +git config user.name "github-actions[bot]" +git config user.email "github-actions[bot]@users.noreply.github.com" + +git checkout -b "$BRANCH" +git add pyproject.toml uv.lock "$FRAGMENT" + +git commit -m "Update PolicyEngine bundle to ${LATEST}" +git push -u origin "$BRANCH" + +gh pr create \ + --base master \ + --title "Update PolicyEngine bundle to ${LATEST}" \ + --body "$PR_BODY" + +echo "PR created: PolicyEngine bundle ${CURRENT} -> ${LATEST}" diff --git a/.github/wait-for-pypi.sh b/.github/wait-for-pypi.sh index b1381b975..17f004b39 100755 --- a/.github/wait-for-pypi.sh +++ b/.github/wait-for-pypi.sh @@ -1,16 +1,16 @@ #! /usr/bin/env bash COMMIT_MSG=$(git log -1 --pretty=%B) -if ! echo "$COMMIT_MSG" | grep -qi "update policyengine us to"; then - echo "Skipping PyPI wait — not an automated policyengine_us update commit." +if ! echo "$COMMIT_MSG" | grep -qi "update policyengine bundle to"; then + echo "Skipping PyPI wait — not an automated PolicyEngine bundle update commit." exit 0 fi -VERSION=$(grep -oP "(?<=policyengine_us==)[0-9\.]+" requirements.txt) -echo "Waiting for policyengine_us version $VERSION to appear on PyPI..." +VERSION=$(grep -oP "(?<=policyengine\\[models\\]==)[0-9\.]+" pyproject.toml) +echo "Waiting for policyengine version $VERSION to appear on PyPI..." for i in {1..6}; do - if curl -s https://pypi.org/pypi/policyengine_us/json | jq -e ".releases[\"$VERSION\"]" > /dev/null; then + if curl -s https://pypi.org/pypi/policyengine/json | jq -e ".releases[\"$VERSION\"]" > /dev/null; then echo "Version $VERSION is available on PyPI!" exit 0 fi @@ -18,5 +18,5 @@ for i in {1..6}; do sleep 10 done -echo "Timed out waiting for policyengine_us version $VERSION to become available on PyPI" +echo "Timed out waiting for policyengine version $VERSION to become available on PyPI" exit 1 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 32800b53e..9b9c4629e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -82,7 +82,7 @@ jobs: with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }} - - name: Wait until policyengine_us version is available on PyPI + - name: Wait until PolicyEngine bundle version is available on PyPI run: .github/wait-for-pypi.sh - name: Install dependencies run: make install diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 6255e276b..3a0120c18 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -48,7 +48,7 @@ jobs: - name: Find API model versions and write to environment variable run: python3 .github/find-api-model-versions.py - name: Ensure full API and simulation API model versions are in sync - run: ".github/request-simulation-model-versions.sh -us ${{ env.US_VERSION }} -uk ${{ env.UK_VERSION }}" + run: ".github/request-simulation-model-versions.sh -py ${{ env.POLICYENGINE_VERSION }} -us ${{ env.US_VERSION }} -uk ${{ env.UK_VERSION }}" env: SIMULATION_API_URL: ${{ secrets.SIMULATION_API_URL }} @@ -376,7 +376,7 @@ jobs: - name: Find API model versions and write to environment variable run: python3 .github/find-api-model-versions.py - name: Ensure full API and simulation API model versions are in sync - run: ".github/request-simulation-model-versions.sh -us ${{ env.US_VERSION }} -uk ${{ env.UK_VERSION }}" + run: ".github/request-simulation-model-versions.sh -py ${{ env.POLICYENGINE_VERSION }} -us ${{ env.US_VERSION }} -uk ${{ env.UK_VERSION }}" env: SIMULATION_API_URL: ${{ secrets.SIMULATION_API_URL }} diff --git a/.github/workflows/update-country-packages.yaml b/.github/workflows/update-policyengine-bundle.yaml similarity index 72% rename from .github/workflows/update-country-packages.yaml rename to .github/workflows/update-policyengine-bundle.yaml index 68e87c15d..d0a643352 100644 --- a/.github/workflows/update-country-packages.yaml +++ b/.github/workflows/update-policyengine-bundle.yaml @@ -1,4 +1,4 @@ -name: Update country packages +name: Update PolicyEngine bundle on: schedule: @@ -7,13 +7,9 @@ on: jobs: update: - name: Update ${{ matrix.package }} + name: Update PolicyEngine bundle runs-on: ubuntu-latest if: github.repository == 'PolicyEngine/policyengine-api' - strategy: - matrix: - package: [policyengine-us, policyengine-uk] - fail-fast: false steps: - name: Generate GitHub App token id: app-token @@ -34,11 +30,11 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install requests + run: pip install requests uv - name: Check for update and open PR env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - chmod +x .github/scripts/update-package.sh - .github/scripts/update-package.sh ${{ matrix.package }} + chmod +x .github/scripts/update-policyengine-package.sh + .github/scripts/update-policyengine-package.sh diff --git a/changelog.d/policyengine-bundle-routing.changed.md b/changelog.d/policyengine-bundle-routing.changed.md new file mode 100644 index 000000000..a02106147 --- /dev/null +++ b/changelog.d/policyengine-bundle-routing.changed.md @@ -0,0 +1 @@ +Route economy simulations through the installed PolicyEngine .py bundle and derive bundled US/UK package versions from it. diff --git a/policyengine_api/constants.py b/policyengine_api/constants.py index fa7b6730b..2c4bfbadd 100644 --- a/policyengine_api/constants.py +++ b/policyengine_api/constants.py @@ -1,7 +1,8 @@ -from pathlib import Path -from importlib.metadata import distributions -from datetime import datetime import hashlib +import json +from datetime import datetime +from importlib.metadata import distribution, distributions +from pathlib import Path REPO = Path(__file__).parents[1] GET = "GET" @@ -18,6 +19,10 @@ "policyengine_ng", "policyengine_il", ) +BUNDLED_COUNTRY_PACKAGE_NAMES = { + "uk": "policyengine-uk", + "us": "policyengine-us", +} def _normalize_distribution_name(name: str | None) -> str: @@ -36,6 +41,46 @@ def _resolve_distribution_version( return "0.0.0" +def get_py_manifest(): + return distribution("policyengine").locate_file( + "policyengine/data/bundle/manifest.json" + ) + + +def _load_policyengine_bundle() -> dict | None: + try: + with open(get_py_manifest()) as manifest_file: + manifest = json.load(manifest_file) + except Exception as exc: + raise RuntimeError( + "Could not read PolicyEngine .py bundle manifest from the installed " + "policyengine wheel." + ) from exc + if not isinstance(manifest, dict): + raise RuntimeError("PolicyEngine .py bundle manifest must be a JSON object.") + return manifest + + +def _resolve_bundle_package_versions(bundle: dict | None) -> dict[str, str]: + if not isinstance(bundle, dict): + return {} + + packages = bundle.get("packages") + if not isinstance(packages, dict): + return {} + + package_versions: dict[str, str] = {} + for package_key, package in packages.items(): + if not isinstance(package, dict): + continue + version = package.get("version") + if version is None: + continue + name = package.get("name") or package_key + package_versions[_normalize_distribution_name(str(name))] = str(version) + return package_versions + + try: _dist_versions = { _normalize_distribution_name(d.metadata["Name"]): d.version @@ -45,12 +90,29 @@ def _resolve_distribution_version( country: _resolve_distribution_version(_dist_versions, package_name) for country, package_name in zip(COUNTRIES, COUNTRY_PACKAGE_NAMES) } - POLICYENGINE_CORE_VERSION = _resolve_distribution_version( - _dist_versions, "policyengine-core", "policyengine" - ) except Exception: + _dist_versions = {} COUNTRY_PACKAGE_VERSIONS = {country: "0.0.0" for country in COUNTRIES} - POLICYENGINE_CORE_VERSION = "0.0.0" + +_policyengine_bundle = _load_policyengine_bundle() +_bundle_package_versions = _resolve_bundle_package_versions(_policyengine_bundle) + +for country, package_name in BUNDLED_COUNTRY_PACKAGE_NAMES.items(): + version = _bundle_package_versions.get(_normalize_distribution_name(package_name)) + if version is not None: + COUNTRY_PACKAGE_VERSIONS[country] = version + +POLICYENGINE_VERSION = _resolve_distribution_version(_dist_versions, "policyengine") +if isinstance(_policyengine_bundle, dict): + bundle_version = _policyengine_bundle.get( + "policyengine_version" + ) or _policyengine_bundle.get("bundle_version") + if bundle_version is not None: + POLICYENGINE_VERSION = str(bundle_version) + +POLICYENGINE_CORE_VERSION = _bundle_package_versions.get( + "policyengine-core" +) or _resolve_distribution_version(_dist_versions, "policyengine-core", "policyengine") RUNTIME_CACHE_SCHEMA_VERSIONS = { "economy_impact": 1, @@ -80,6 +142,7 @@ def _build_runtime_cache_version( country_id, caller_version or COUNTRY_PACKAGE_VERSIONS.get(country_id, "0.0.0"), COUNTRY_PACKAGE_VERSIONS.get(country_id, "0.0.0"), + POLICYENGINE_VERSION, POLICYENGINE_CORE_VERSION, schema_version, ) diff --git a/policyengine_api/data/model_setup.py b/policyengine_api/data/model_setup.py deleted file mode 100644 index e804ac4cf..000000000 --- a/policyengine_api/data/model_setup.py +++ /dev/null @@ -1,20 +0,0 @@ -ENHANCED_FRS = ( - "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2023_24.h5@1.40.3" -) -FRS = "hf://policyengine/policyengine-uk-data-private/frs_2023_24.h5@1.40.3" - -ENHANCED_CPS = "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.77.0" -CPS = "hf://policyengine/policyengine-us-data/cps_2023.h5@1.77.0" -POOLED_CPS = "hf://policyengine/policyengine-us-data/pooled_3_year_cps_2023.h5@1.77.0" - -datasets = { - "uk": { - "enhanced_frs": ENHANCED_FRS, - "frs": FRS, - }, - "us": { - "enhanced_cps": ENHANCED_CPS, - "cps": CPS, - "pooled_cps": POOLED_CPS, - }, -} diff --git a/policyengine_api/libs/simulation_api_modal.py b/policyengine_api/libs/simulation_api_modal.py index 0ea8900f9..ba996fba4 100644 --- a/policyengine_api/libs/simulation_api_modal.py +++ b/policyengine_api/libs/simulation_api_modal.py @@ -11,7 +11,6 @@ from typing import Optional import httpx - from policyengine_api.gcp_logging import logger from policyengine_api.libs.gateway_auth import ( GatewayAuthError, @@ -93,7 +92,7 @@ def __init__(self): "GATEWAY_AUTH_CLIENT_SECRET." ) print( - "SimulationAPIModal initialised without gateway auth; " + "SimulationAPIModal initialized without gateway auth; " "all GATEWAY_AUTH_* env vars are unset and " "GATEWAY_AUTH_REQUIRED is not enabled.", file=sys.stderr, @@ -101,6 +100,15 @@ def __init__(self): ) self.client = httpx.Client(timeout=30.0, auth=auth) + def _normalize_submission_payload(self, payload: dict) -> dict: + modal_payload = { + key: value for key, value in payload.items() if value is not None + } + if "model_version" in modal_payload: + modal_payload["version"] = modal_payload.pop("model_version") + modal_payload.pop("data_version", None) + return modal_payload + def run(self, payload: dict) -> ModalSimulationExecution: """ Submit a simulation job to the Modal API. @@ -109,7 +117,7 @@ def run(self, payload: dict) -> ModalSimulationExecution: ---------- payload : dict The simulation parameters (country, reform, baseline, etc.) - Expected to match SimulationOptions schema. + Expected to match the simulation gateway submission schema. Returns ------- @@ -122,13 +130,7 @@ def run(self, payload: dict) -> ModalSimulationExecution: If the API returns an error response. """ try: - # Map field names from SimulationOptions to Modal API format - # SimulationOptions uses 'model_version', Modal expects 'version' - modal_payload = dict(payload) - if "model_version" in modal_payload: - modal_payload["version"] = modal_payload.pop("model_version") - # Remove data_version as Modal doesn't use it - modal_payload.pop("data_version", None) + modal_payload = self._normalize_submission_payload(payload) response = self.client.post( f"{self.base_url}/simulate/economy/comparison", @@ -181,10 +183,7 @@ def run_budget_window_batch(self, payload: dict) -> ModalBudgetWindowBatchExecut Submit a budget-window batch job to the Modal API. """ try: - modal_payload = dict(payload) - if "model_version" in modal_payload: - modal_payload["version"] = modal_payload.pop("model_version") - modal_payload.pop("data_version", None) + modal_payload = self._normalize_submission_payload(payload) response = self.client.post( f"{self.base_url}/simulate/economy/budget-window", @@ -228,9 +227,25 @@ def run_budget_window_batch(self, payload: dict) -> ModalBudgetWindowBatchExecut raise def resolve_app_name( - self, country: str, version: Optional[str] = None + self, + country: str, + version: Optional[str] = None, + policyengine_version: Optional[str] = None, ) -> tuple[str, str]: """Resolve the current gateway app name for a country/model version.""" + if policyengine_version is not None: + response = self.client.get(f"{self.base_url}/versions/policyengine") + response.raise_for_status() + policyengine_version_map = response.json() + try: + return policyengine_version_map[policyengine_version], ( + version or policyengine_version + ) + except KeyError as exc: + raise ValueError( + f"Unknown policyengine version {policyengine_version}" + ) from exc + response = self.client.get(f"{self.base_url}/versions/{country}") response.raise_for_status() version_map = response.json() diff --git a/policyengine_api/services/economy_service.py b/policyengine_api/services/economy_service.py index 59a51403a..820b04f1a 100644 --- a/policyengine_api/services/economy_service.py +++ b/policyengine_api/services/economy_service.py @@ -1,39 +1,36 @@ -from policyengine_api.services.policy_service import PolicyService -from policyengine_api.services.reform_impacts_service import ( - ReformImpactsService, -) -from policyengine_api.services.budget_window_cache import BudgetWindowCache +import datetime +import hashlib +import json +import uuid +from enum import Enum +from typing import Annotated, Any, Literal, Optional + +import httpx +import numpy as np +from dotenv import load_dotenv from policyengine_api.constants import ( COUNTRY_PACKAGE_VERSIONS, - EXECUTION_STATUSES_SUCCESS, EXECUTION_STATUSES_FAILURE, EXECUTION_STATUSES_PENDING, + EXECUTION_STATUSES_SUCCESS, + POLICYENGINE_VERSION, get_economy_impact_cache_version, ) -from policyengine_api.gcp_logging import logger -from policyengine_api.libs.simulation_api_modal import simulation_api_modal -from policyengine_api.data.model_setup import ( - datasets as configured_datasets, -) from policyengine_api.data.congressional_districts import ( - get_valid_state_codes, get_valid_congressional_districts, + get_valid_state_codes, normalize_us_region, ) from policyengine_api.data.places import validate_place_code +from policyengine_api.gcp_logging import logger +from policyengine_api.libs.simulation_api_modal import simulation_api_modal +from policyengine_api.services.budget_window_cache import BudgetWindowCache +from policyengine_api.services.policy_service import PolicyService +from policyengine_api.services.reform_impacts_service import ( + ReformImpactsService, +) from policyengine_api.utils import budget_window as budget_window_utils -from policyengine.simulation import SimulationOptions -from policyengine.utils.data.datasets import get_default_dataset -import httpx -import json -import datetime -import hashlib -import uuid -from typing import Literal, Any, Optional, Annotated -from dotenv import load_dotenv from pydantic import BaseModel, Field -import numpy as np -from enum import Enum load_dotenv() @@ -43,16 +40,6 @@ budget_window_cache = BudgetWindowCache() -def get_policyengine_version() -> str | None: - """Legacy test seam; runtime bundle metadata comes from the simulation API.""" - return None - - -def get_dataset_version(country_id: str) -> str | None: - """Legacy test seam; runtime bundle metadata comes from the simulation API.""" - return None - - class ImpactAction(Enum): """ Enum for the action to take based on the status of an economic impact calculation. @@ -81,6 +68,20 @@ class ImpactStatus(Enum): BUDGET_WINDOW_SUBMISSION_VALIDATION_ERROR_STATUS_CODES = {400, 422} +class SimulationOptions(BaseModel): + country: str + scope: Literal["macro", "household"] = "macro" + reform: dict[str, Any] + baseline: dict[str, Any] + time_period: str | int + include_cliffs: bool = False + region: str + data: str | None = None + model_version: str | None = None + policyengine_version: str | None = None + data_version: str | None = None + + class EconomicImpactSetupOptions(BaseModel): process_id: str country_id: str @@ -419,6 +420,7 @@ def _build_budget_window_batch_payload( scope="macro", include_cliffs=False, model_version=setup_options.model_version, + policyengine_version=setup_options.policyengine_version, data_version=setup_options.data_version, ) sim_params = sim_config.model_dump() @@ -575,16 +577,17 @@ def _build_economic_impact_setup_options( process_id: str = self._create_process_id() cache_version = get_economy_impact_cache_version(country_id, api_version) country_package_version = COUNTRY_PACKAGE_VERSIONS.get(country_id) - resolved_dataset = self._setup_data( - country_id=country_id, - region=region, - dataset=dataset, - ) + resolved_dataset = self._canonical_dataset(dataset) resolved_data_version = self._extract_dataset_version(resolved_dataset) + policyengine_version = ( + POLICYENGINE_VERSION if country_id in {"us", "uk"} else None + ) options_hash = self._build_options_hash( options=options, model_version=country_package_version, dataset=resolved_dataset, + data_version=resolved_data_version, + policyengine_version=policyengine_version, ) return EconomicImpactSetupOptions.model_validate( @@ -600,7 +603,7 @@ def _build_economic_impact_setup_options( "api_version": cache_version, "target": target, "model_version": country_package_version, - "policyengine_version": None, + "policyengine_version": policyengine_version, "data_version": resolved_data_version, "runtime_app_name": None, "options_hash": options_hash, @@ -689,6 +692,7 @@ def _resolve_runtime_bundle_for_setup_options( ) = simulation_api.resolve_app_name( setup_options.country_id, setup_options.model_version, + policyengine_version=setup_options.policyengine_version, ) setup_options.options_hash = self._build_options_hash( @@ -905,6 +909,7 @@ def _handle_create_impact( include_cliffs=setup_options.target == "cliff", model_version=setup_options.model_version, data_version=setup_options.data_version, + policyengine_version=setup_options.policyengine_version, ) sim_params = sim_config.model_dump(mode="json") @@ -973,6 +978,7 @@ def _setup_sim_options( scope: Literal["macro", "household"] = "macro", include_cliffs: bool = False, model_version: str | None = None, + policyengine_version: str | None = None, data_version: str | None = None, dataset: str = "default", ) -> SimulationOptions: @@ -993,6 +999,7 @@ def _setup_sim_options( country_id=country_id, region=region, dataset=dataset ), "model_version": model_version, + "policyengine_version": policyengine_version, "data_version": data_version, } ) @@ -1027,7 +1034,9 @@ def _build_options_hash_lookup_pattern(self, options_hash: str) -> str: return f"{escaped_options_hash[:-1]}&%" return f"{escaped_options_hash}%" - def _extract_dataset_version(self, dataset: str) -> str | None: + def _extract_dataset_version(self, dataset: str | None) -> str | None: + if dataset is None: + return None if "@" not in dataset: return None return dataset.rsplit("@", 1)[1] @@ -1052,6 +1061,7 @@ def _should_refresh_cached_impact( runtime_app_name, resolved_model_version = simulation_api.resolve_app_name( setup_options.country_id, setup_options.model_version, + policyengine_version=setup_options.policyengine_version, ) except Exception: return False @@ -1097,7 +1107,7 @@ def _with_policyengine_bundle( } if isinstance(result.get("policyengine_bundle"), dict): for key, value in result["policyengine_bundle"].items(): - if bundle.get(key) is None and value is not None: + if value is not None: bundle[key] = value execution_bundle = ( getattr(execution, "policyengine_bundle", None) @@ -1157,47 +1167,38 @@ def _validate_us_region(self, region: str) -> None: else: raise ValueError(f"Invalid US region: '{region}'") - # Dataset keywords that are passed directly to the simulation API - # instead of being resolved via get_default_dataset + # Dataset keywords that are passed directly to the simulation API. PASSTHROUGH_DATASETS = { "national-with-breakdowns", "national-with-breakdowns-test", } + def _canonical_dataset(self, dataset: str | None = "default") -> str: + if not dataset: + return "default" + return dataset + def _setup_data( self, country_id: str, region: str, dataset: str = "default" - ) -> str: + ) -> str | None: """ - Determine the dataset to use based on the country and region. + Determine the dataset value to send to the simulation gateway. - If the dataset is in PASSTHROUGH_DATASETS, it will be passed directly - to the simulation API. If the dataset matches a configured dataset alias - for the country, resolve it to the published dataset URI. Otherwise, - uses policyengine's get_default_dataset to resolve the appropriate GCS - path. + Default requests intentionally omit ``data`` so the gateway resolves + the certified dataset from the requested .py bundle. Explicit dataset + values are retained as a legacy escape hatch for callers that still pass + dataset designators or full dataset URIs. """ - # If the dataset is a recognized passthrough keyword, use it directly + if dataset in (None, "", "default"): + return None + if dataset in self.PASSTHROUGH_DATASETS: return dataset if "://" in dataset: return dataset - # Resolve explicit dataset aliases exposed in metadata. - country_datasets = configured_datasets.get(country_id, {}) - if dataset in country_datasets: - return country_datasets[dataset] - - try: - return get_default_dataset(country_id, region) - except ValueError as e: - logger.log_struct( - { - "message": f"Error getting default dataset for country={country_id}, region={region}: {str(e)}", - }, - severity="ERROR", - ) - raise + return dataset def _build_simulation_telemetry( self, diff --git a/pyproject.toml b/pyproject.toml index 8c159f084..d0119f2cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,7 @@ dependencies = [ "policyengine_canada==0.96.3", "policyengine-ng==0.5.1", "policyengine-il==0.1.0", - "policyengine_uk==2.88.20", - "policyengine_us==1.715.2", - "policyengine_core>=3.23.5", - "policyengine>0.12.0,<1", + "policyengine[models]==4.18.3", "pydantic", "pymysql", "python-dotenv", diff --git a/tests/contract/test_simulation_gateway_contract.py b/tests/contract/test_simulation_gateway_contract.py index 1716577af..0c13587bd 100644 --- a/tests/contract/test_simulation_gateway_contract.py +++ b/tests/contract/test_simulation_gateway_contract.py @@ -1,10 +1,10 @@ from unittest.mock import MagicMock import httpx -import pytest - import policyengine_api.libs.simulation_api_modal as simulation_api_modal +import pytest from policyengine_api.libs.simulation_api_modal import SimulationAPIModal + from tests.fixtures.libs.simulation_api_modal import ( MOCK_BATCH_JOB_ID, MOCK_BATCH_POLL_RESPONSE_COMPLETE, @@ -129,6 +129,10 @@ def test_gateway_versions_and_health_contract(monkeypatch): status_code=200, json_data={"latest": "1.702.0", "1.702.0": MOCK_RESOLVED_APP_NAME}, ), + ("GET", "/versions/policyengine"): _response( + status_code=200, + json_data={"latest": "4.18.3", "4.18.3": MOCK_RESOLVED_APP_NAME}, + ), ("GET", "/health"): _response( status_code=200, json_data=MOCK_HEALTH_RESPONSE, @@ -137,7 +141,14 @@ def test_gateway_versions_and_health_contract(monkeypatch): ) app_name, version = client.resolve_app_name("us") + bundle_app_name, bundle_version = client.resolve_app_name( + "us", + "1.729.0", + policyengine_version="4.18.3", + ) assert app_name == MOCK_RESOLVED_APP_NAME assert version == "1.702.0" + assert bundle_app_name == MOCK_RESOLVED_APP_NAME + assert bundle_version == "1.729.0" assert client.health_check() is True diff --git a/tests/fixtures/services/economy_service.py b/tests/fixtures/services/economy_service.py index 49202132d..0a75cd81a 100644 --- a/tests/fixtures/services/economy_service.py +++ b/tests/fixtures/services/economy_service.py @@ -1,11 +1,11 @@ -import pytest -from unittest.mock import patch, MagicMock -import json import datetime +import json +from unittest.mock import MagicMock, patch +import pytest from policyengine_api.constants import ( - MODAL_EXECUTION_STATUS_SUBMITTED, MODAL_EXECUTION_STATUS_RUNNING, + MODAL_EXECUTION_STATUS_SUBMITTED, ) # Mock data constants @@ -13,7 +13,7 @@ MOCK_POLICY_ID = 123 MOCK_BASELINE_POLICY_ID = 456 MOCK_REGION = "us" -MOCK_DATASET = "enhanced_cps" +MOCK_DATASET = "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.77.0" MOCK_TIME_PERIOD = "2025" MOCK_API_VERSION = "1.0" MOCK_OPTIONS = {"option1": "value1", "option2": "value2"} @@ -21,11 +21,12 @@ MOCK_LOOKUP_OPTIONS_HASH = ( "[option1=value1&option2=value2" "&dataset=hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.77.0" - "&model_version=1.2.3]" + "&model_version=1.2.3" + "&data_version=1.77.0" + "&policyengine_version=3.4.0]" ) MOCK_OPTIONS_HASH = ( MOCK_LOOKUP_OPTIONS_HASH[:-1] - + "&data_version=1.77.0" + "&runtime_app_name=policyengine-simulation-us1-2-3-uk2-7-8]" ) MOCK_MODAL_JOB_ID = "fc-test123xyz" @@ -62,9 +63,10 @@ "region": MOCK_REGION, "time_period": MOCK_TIME_PERIOD, "scope": "macro", - "dataset": MOCK_RESOLVED_DATASET, + "data": MOCK_RESOLVED_DATASET, "include_cliffs": False, "model_version": MOCK_MODEL_VERSION, + "policyengine_version": MOCK_POLICYENGINE_VERSION, "data_version": MOCK_DATA_VERSION, } @@ -80,21 +82,11 @@ def mock_country_package_versions(): @pytest.fixture -def mock_get_dataset_version(): - """Mock get_dataset_version function.""" - with patch( - "policyengine_api.services.economy_service.get_dataset_version", - return_value=MOCK_DATA_VERSION, - ) as mock: - yield mock - - -@pytest.fixture -def mock_get_policyengine_version(): - """Mock get_policyengine_version function.""" +def mock_policyengine_version(): + """Mock the PolicyEngine .py bundle version used by economy routing.""" with patch( - "policyengine_api.services.economy_service.get_policyengine_version", - return_value=MOCK_POLICYENGINE_VERSION, + "policyengine_api.services.economy_service.POLICYENGINE_VERSION", + MOCK_POLICYENGINE_VERSION, ) as mock: yield mock @@ -142,9 +134,11 @@ def mock_simulation_api(): mock_api._setup_sim_options.return_value = MOCK_SIM_CONFIG mock_api.run.return_value = mock_execution - mock_api.resolve_app_name.side_effect = lambda country_id, version=None: ( - MOCK_RESOLVED_APP_NAME, - version or MOCK_MODEL_VERSION, + mock_api.resolve_app_name.side_effect = ( + lambda country_id, version=None, policyengine_version=None: ( + MOCK_RESOLVED_APP_NAME, + version or MOCK_MODEL_VERSION, + ) ) mock_api.get_execution_id.return_value = MOCK_MODAL_JOB_ID mock_api.get_execution_by_id.return_value = mock_execution @@ -319,9 +313,11 @@ def mock_simulation_api_modal(): mock_execution = create_mock_modal_execution() mock_api.run.return_value = mock_execution - mock_api.resolve_app_name.side_effect = lambda country_id, version=None: ( - MOCK_RESOLVED_APP_NAME, - version or MOCK_MODEL_VERSION, + mock_api.resolve_app_name.side_effect = ( + lambda country_id, version=None, policyengine_version=None: ( + MOCK_RESOLVED_APP_NAME, + version or MOCK_MODEL_VERSION, + ) ) mock_api.get_execution_id.return_value = MOCK_MODAL_JOB_ID mock_api.get_execution_by_id.return_value = mock_execution @@ -332,52 +328,3 @@ def mock_simulation_api_modal(): "policyengine_api.services.economy_service.simulation_api", mock_api ) as mock: yield mock - - -# Expected GCS paths from get_default_dataset -MOCK_US_NATIONWIDE_DATASET = "gs://policyengine-us-data/cps_2023.h5" -MOCK_US_STATE_CA_DATASET = "gs://policyengine-us-data/states/CA.h5" -MOCK_US_STATE_UT_DATASET = "gs://policyengine-us-data/states/UT.h5" -MOCK_US_PLACE_NJ_57000_DATASET = "gs://policyengine-us-data/states/NJ.h5" -MOCK_US_DISTRICT_CA37_DATASET = "gs://policyengine-us-data/districts/CA-37.h5" -MOCK_UK_DATASET = "gs://policyengine-uk-data-private/enhanced_frs_2023_24.h5" - - -def mock_get_default_dataset_fn(country: str, region: str | None) -> str: - """Mock implementation of get_default_dataset for testing.""" - if country == "uk": - return MOCK_UK_DATASET - elif country == "us": - if region == "us" or region is None: - return MOCK_US_NATIONWIDE_DATASET - elif region == "state/ca": - return MOCK_US_STATE_CA_DATASET - elif region == "state/ut": - return MOCK_US_STATE_UT_DATASET - elif region.startswith("place/"): - # Place uses parent state's dataset - place_code = region.split("/")[1] - state_abbrev = place_code.split("-")[0].upper() - return f"gs://policyengine-us-data/states/{state_abbrev}.h5" - elif region == "congressional_district/CA-37": - return MOCK_US_DISTRICT_CA37_DATASET - elif region.startswith("state/"): - # Generic state handling - state_code = region.split("/")[1].upper() - return f"gs://policyengine-us-data/states/{state_code}.h5" - elif region.startswith("congressional_district/"): - district_id = region.split("/")[1].upper() - return f"gs://policyengine-us-data/districts/{district_id}.h5" - else: - return MOCK_US_NATIONWIDE_DATASET - raise ValueError(f"Unknown country: {country}") - - -@pytest.fixture -def mock_get_default_dataset(): - """Mock get_default_dataset function.""" - with patch( - "policyengine_api.services.economy_service.get_default_dataset", - side_effect=mock_get_default_dataset_fn, - ) as mock: - yield mock diff --git a/tests/unit/data/test_model_setup.py b/tests/unit/data/test_model_setup.py deleted file mode 100644 index 1bbabef28..000000000 --- a/tests/unit/data/test_model_setup.py +++ /dev/null @@ -1,36 +0,0 @@ -from policyengine_api.data.model_setup import ( - CPS, - ENHANCED_CPS, - ENHANCED_FRS, - FRS, - POOLED_CPS, - datasets, -) - - -class TestDatasets: - def test__given_us_aliases__then_returns_versioned_public_hf_uris(self): - assert datasets["us"] == { - "enhanced_cps": ENHANCED_CPS, - "cps": CPS, - "pooled_cps": POOLED_CPS, - } - assert ENHANCED_CPS.endswith("@1.77.0") - assert CPS.endswith("@1.77.0") - assert POOLED_CPS.endswith("@1.77.0") - - def test__given_uk_aliases__then_returns_versioned_private_hf_uris(self): - assert datasets["uk"] == { - "enhanced_frs": ENHANCED_FRS, - "frs": FRS, - } - assert ENHANCED_FRS.startswith( - "hf://policyengine/policyengine-uk-data-private/" - ) - assert FRS.startswith("hf://policyengine/policyengine-uk-data-private/") - assert ENHANCED_FRS.endswith("@1.40.3") - assert FRS.endswith("@1.40.3") - - def test__given_unknown_country__then_has_no_dataset_aliases(self): - assert "ca" not in datasets - assert "invalid" not in datasets diff --git a/tests/unit/libs/test_simulation_api_modal.py b/tests/unit/libs/test_simulation_api_modal.py index 300badee5..93672979f 100644 --- a/tests/unit/libs/test_simulation_api_modal.py +++ b/tests/unit/libs/test_simulation_api_modal.py @@ -8,8 +8,7 @@ import os import sys from types import SimpleNamespace -from unittest.mock import patch -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import httpx import pytest @@ -20,36 +19,37 @@ ) os.environ.setdefault("FLASK_DEBUG", "1") -from policyengine_api.libs.simulation_api_modal import ( # noqa: E402 - ModalBudgetWindowBatchExecution, - ModalSimulationExecution, - SimulationAPIModal, -) from policyengine_api.constants import ( # noqa: E402 MODAL_EXECUTION_STATUS_COMPLETE, MODAL_EXECUTION_STATUS_FAILED, MODAL_EXECUTION_STATUS_RUNNING, MODAL_EXECUTION_STATUS_SUBMITTED, ) +from policyengine_api.libs.simulation_api_modal import ( # noqa: E402 + ModalBudgetWindowBatchExecution, + ModalSimulationExecution, + SimulationAPIModal, +) + from tests.fixtures.libs.simulation_api_modal import ( # noqa: E402 - MOCK_MODAL_JOB_ID, MOCK_BATCH_JOB_ID, + MOCK_BATCH_POLL_RESPONSE_COMPLETE, + MOCK_BATCH_POLL_RESPONSE_FAILED, + MOCK_BATCH_POLL_RESPONSE_RUNNING, + MOCK_BATCH_SUBMIT_RESPONSE_SUCCESS, + MOCK_HEALTH_RESPONSE, MOCK_MODAL_BASE_URL, + MOCK_MODAL_JOB_ID, + MOCK_POLICYENGINE_BUNDLE, + MOCK_POLL_RESPONSE_COMPLETE, + MOCK_POLL_RESPONSE_FAILED, + MOCK_POLL_RESPONSE_RUNNING, + MOCK_RESOLVED_APP_NAME, + MOCK_RUN_ID, MOCK_SIMULATION_PAYLOAD, MOCK_SIMULATION_PAYLOAD_WITH_TELEMETRY, - MOCK_RUN_ID, MOCK_SIMULATION_RESULT, - MOCK_POLICYENGINE_BUNDLE, - MOCK_RESOLVED_APP_NAME, MOCK_SUBMIT_RESPONSE_SUCCESS, - MOCK_POLL_RESPONSE_RUNNING, - MOCK_POLL_RESPONSE_COMPLETE, - MOCK_POLL_RESPONSE_FAILED, - MOCK_HEALTH_RESPONSE, - MOCK_BATCH_SUBMIT_RESPONSE_SUCCESS, - MOCK_BATCH_POLL_RESPONSE_RUNNING, - MOCK_BATCH_POLL_RESPONSE_COMPLETE, - MOCK_BATCH_POLL_RESPONSE_FAILED, create_mock_httpx_response, ) @@ -302,6 +302,7 @@ def test__given_model_and_data_versions__then_translates_payload_for_modal( payload = { **MOCK_SIMULATION_PAYLOAD, "model_version": "1.459.0", + "policyengine_version": "4.18.3", "data_version": "1.77.0", } api = SimulationAPIModal() @@ -310,6 +311,69 @@ def test__given_model_and_data_versions__then_translates_payload_for_modal( posted_payload = mock_httpx_client.post.call_args.kwargs["json"] assert posted_payload["version"] == "1.459.0" + assert posted_payload["policyengine_version"] == "4.18.3" + assert "model_version" not in posted_payload + assert "data_version" not in posted_payload + + def test__given_api_v1_default_bundle_payload__then_posts_gateway_contract_body( + self, + mock_httpx_client, + mock_modal_logger, + ): + mock_httpx_client.post.return_value = create_mock_httpx_response( + status_code=202, + json_data=MOCK_SUBMIT_RESPONSE_SUCCESS, + ) + payload = { + "country": "us", + "scope": "macro", + "reform": {"gov.irs.income.bracket.rates.2": {"2026-01-01": 0.24}}, + "baseline": {}, + "time_period": "2026", + "region": "state/ca", + "data": None, + "include_cliffs": False, + "model_version": "1.729.0", + "policyengine_version": "4.18.3", + "data_version": None, + "_metadata": { + "process_id": "job_20260629120000_1234", + "model_version": "1.729.0", + "policyengine_version": "4.18.3", + "data_version": None, + "dataset": "default", + "resolved_app_name": "policyengine-simulation-py4-18-3", + }, + "_telemetry": { + "run_id": "run_20260629120000_1234", + "process_id": "job_20260629120000_1234", + "capture_mode": "disabled", + }, + } + expected_gateway_keys = { + "country", + "scope", + "reform", + "baseline", + "time_period", + "region", + "include_cliffs", + "version", + "policyengine_version", + "_metadata", + "_telemetry", + } + api = SimulationAPIModal() + + api.run(payload) + + posted_payload = mock_httpx_client.post.call_args.kwargs["json"] + assert set(posted_payload) == expected_gateway_keys + assert posted_payload["version"] == "1.729.0" + assert posted_payload["policyengine_version"] == "4.18.3" + assert posted_payload["region"] == "state/ca" + assert posted_payload["_metadata"]["dataset"] == "default" + assert "data" not in posted_payload assert "model_version" not in posted_payload assert "data_version" not in posted_payload @@ -386,6 +450,32 @@ def test__given_unknown_version__then_raises_value_error( ): api.resolve_app_name("us", "9.9.9") + def test__given_policyengine_version__then_returns_registered_bundle_app( + self, + mock_httpx_client, + mock_modal_logger, + ): + mock_httpx_client.get.return_value = create_mock_httpx_response( + status_code=200, + json_data={ + "latest": "4.18.3", + "4.18.3": MOCK_RESOLVED_APP_NAME, + }, + ) + api = SimulationAPIModal() + + app_name, resolved_version = api.resolve_app_name( + "us", + "1.729.0", + policyengine_version="4.18.3", + ) + + assert app_name == MOCK_RESOLVED_APP_NAME + assert resolved_version == "1.729.0" + mock_httpx_client.get.assert_called_once_with( + f"{api.base_url}/versions/policyengine" + ) + class TestRunBudgetWindowBatch: def test__given_valid_payload__then_returns_batch_execution( self, @@ -417,6 +507,7 @@ def test__given_model_and_data_versions__then_translates_payload_for_modal( payload = { **MOCK_SIMULATION_PAYLOAD, "model_version": "1.459.0", + "policyengine_version": "4.18.3", "data_version": "1.77.0", } api = SimulationAPIModal() @@ -425,6 +516,7 @@ def test__given_model_and_data_versions__then_translates_payload_for_modal( posted_payload = mock_httpx_client.post.call_args.kwargs["json"] assert posted_payload["version"] == "1.459.0" + assert posted_payload["policyengine_version"] == "4.18.3" assert "model_version" not in posted_payload assert "data_version" not in posted_payload diff --git a/tests/unit/services/test_economy_service.py b/tests/unit/services/test_economy_service.py index c82d2bd31..a6df29a85 100644 --- a/tests/unit/services/test_economy_service.py +++ b/tests/unit/services/test_economy_service.py @@ -1,92 +1,38 @@ import json -import sys -import httpx -import pytest -from unittest.mock import patch, MagicMock from typing import Literal -from types import ModuleType - -try: - from policyengine.simulation import SimulationOptions # noqa: F401 -except ModuleNotFoundError: - policyengine_module = sys.modules.setdefault( - "policyengine", ModuleType("policyengine") - ) - simulation_module = ModuleType("policyengine.simulation") - utils_module = ModuleType("policyengine.utils") - data_module = ModuleType("policyengine.utils.data") - datasets_module = ModuleType("policyengine.utils.data.datasets") - - class _StubSimulationOptions: - def __init__(self, payload): - self._payload = payload - - @classmethod - def model_validate(cls, payload): - return cls(payload) - - def model_dump(self): - return dict(self._payload) - - simulation_module.SimulationOptions = _StubSimulationOptions - policyengine_module.simulation = simulation_module - - def _stub_get_default_dataset(country, region): - if country == "us": - if region == "us": - return "gs://policyengine-us-data/enhanced_cps_2024.h5" - if region == "state/ca": - return "gs://policyengine-us-data/states/CA.h5" - if region == "state/ut": - return "gs://policyengine-us-data/states/UT.h5" - if region == "place/NJ-57000": - return "gs://policyengine-us-data/states/NJ.h5" - if region == "congressional_district/CA-37": - return "gs://policyengine-us-data/districts/CA-37.h5" - if country == "uk" and region == "uk": - return "gs://policyengine-uk-data-private/enhanced_frs_2023_24.h5" - raise ValueError( - f"Error getting default dataset for country={country}, region={region}: unsupported in test stub" - ) - - datasets_module.get_default_dataset = _stub_get_default_dataset - data_module.datasets = datasets_module - utils_module.data = data_module - policyengine_module.utils = utils_module - sys.modules["policyengine.simulation"] = simulation_module - sys.modules["policyengine.utils"] = utils_module - sys.modules["policyengine.utils.data"] = data_module - sys.modules["policyengine.utils.data.datasets"] = datasets_module +from unittest.mock import MagicMock, patch +import httpx +import pytest from policyengine_api.services.economy_service import ( BUDGET_WINDOW_MAX_END_YEAR, BUDGET_WINDOW_MAX_YEARS, - EconomyService, EconomicImpactResult, EconomicImpactSetupOptions, + EconomyService, ImpactAction, ImpactStatus, ) from tests.fixtures.services.economy_service import ( + MOCK_API_VERSION, + MOCK_BASELINE_POLICY_ID, MOCK_COUNTRY_ID, MOCK_DATA_VERSION, - MOCK_POLICY_ID, - MOCK_BASELINE_POLICY_ID, - MOCK_REGION, MOCK_DATASET, - MOCK_TIME_PERIOD, - MOCK_API_VERSION, + MOCK_EXECUTION_ID, + MOCK_LOOKUP_OPTIONS_HASH, MOCK_MODEL_VERSION, - MOCK_POLICYENGINE_VERSION, MOCK_OPTIONS, - MOCK_LOOKUP_OPTIONS_HASH, MOCK_OPTIONS_HASH, - MOCK_EXECUTION_ID, + MOCK_POLICY_ID, + MOCK_POLICYENGINE_VERSION, MOCK_PROCESS_ID, - MOCK_RUN_ID, MOCK_REFORM_IMPACT_DATA, - MOCK_RESOLVED_DATASET, + MOCK_REGION, MOCK_RESOLVED_APP_NAME, + MOCK_RESOLVED_DATASET, + MOCK_RUN_ID, + MOCK_TIME_PERIOD, create_mock_budget_window_batch_execution, create_mock_reform_impact, ) @@ -157,8 +103,7 @@ def test__given_completed_impact__returns_completed_result( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -194,8 +139,7 @@ def test__given_legacy_completed_impact__refreshes_cache( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -218,6 +162,7 @@ def test__given_legacy_completed_impact__refreshes_cache( mock_simulation_api.resolve_app_name.assert_called_once_with( MOCK_COUNTRY_ID, MOCK_MODEL_VERSION, + policyengine_version=MOCK_POLICYENGINE_VERSION, ) mock_simulation_api.run.assert_called_once() @@ -226,8 +171,7 @@ def test__given_computing_impact_with_succeeded_execution__returns_completed_res economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -266,8 +210,7 @@ def test__given_computing_impact_with_failed_execution__returns_error_result( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -292,8 +235,7 @@ def test__given_computing_impact_with_active_execution__returns_computing_result economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -317,8 +259,7 @@ def test__given_no_previous_impact__creates_new_simulation( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -340,8 +281,7 @@ def test__given_no_previous_impact__includes_metadata_in_simulation_params( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -366,7 +306,10 @@ def test__given_no_previous_impact__includes_metadata_in_simulation_params( ) assert sim_params["_metadata"]["process_id"] == MOCK_PROCESS_ID assert sim_params["_metadata"]["model_version"] == MOCK_MODEL_VERSION - assert sim_params["_metadata"]["policyengine_version"] is None + assert ( + sim_params["_metadata"]["policyengine_version"] + == MOCK_POLICYENGINE_VERSION + ) assert sim_params["_metadata"]["data_version"] == MOCK_DATA_VERSION assert sim_params["_metadata"]["dataset"] == MOCK_RESOLVED_DATASET assert ( @@ -378,7 +321,6 @@ def test__given_no_previous_impact__includes_telemetry_in_simulation_params( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -410,8 +352,7 @@ def test__given_runtime_cache_version__uses_versioned_economy_cache_key( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -448,8 +389,7 @@ def test__given_alias_dataset__queries_previous_impacts_with_resolved_bundle( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -467,7 +407,8 @@ def test__given_alias_dataset__queries_previous_impacts_with_resolved_bundle( assert call_args[7] == economy_service._build_options_hash_lookup_pattern( MOCK_LOOKUP_OPTIONS_HASH ) - assert "data_version=" not in call_args[7] + assert "data\\_version=1.77.0" in call_args[7] + assert "policyengine\\_version=3.4.0" in call_args[7] assert "runtime_app_name" not in call_args[7] def test__given_completed_impact__uses_resolved_runtime_bundle_for_cache_lookup( @@ -475,8 +416,7 @@ def test__given_completed_impact__uses_resolved_runtime_bundle_for_cache_lookup( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -495,6 +435,7 @@ def test__given_completed_impact__uses_resolved_runtime_bundle_for_cache_lookup( mock_simulation_api.resolve_app_name.assert_called_once_with( MOCK_COUNTRY_ID, MOCK_MODEL_VERSION, + policyengine_version=MOCK_POLICYENGINE_VERSION, ) def test__given_cached_impact_and_runtime_lookup_fails__then_returns_cached_result( @@ -502,8 +443,7 @@ def test__given_cached_impact_and_runtime_lookup_fails__then_returns_cached_resu economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -532,8 +472,7 @@ def test__given_legacy_cached_impact_without_resolved_app_name__then_refreshes_c economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -556,6 +495,7 @@ def test__given_legacy_cached_impact_without_resolved_app_name__then_refreshes_c mock_simulation_api.resolve_app_name.assert_called_once_with( MOCK_COUNTRY_ID, MOCK_MODEL_VERSION, + policyengine_version=MOCK_POLICYENGINE_VERSION, ) mock_simulation_api.run.assert_called_once() @@ -564,8 +504,7 @@ def test__given_legacy_and_refreshed_cached_impacts__then_reuses_refreshed_entry economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -604,8 +543,7 @@ def test__given_legacy_cached_impact_and_runtime_lookup_fails__then_returns_cach economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -636,8 +574,7 @@ def test__given_legacy_computing_impact_without_resolved_app_name__then_reuses_e economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -665,8 +602,7 @@ def test__given_exception__raises_error( economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -686,8 +622,7 @@ def test__given_uk_request__preserves_model_version_in_bundle( self, economy_service, mock_country_package_versions, - mock_get_dataset_version, - mock_get_policyengine_version, + mock_policyengine_version, mock_policy_service, mock_reform_impacts_service, mock_simulation_api, @@ -718,7 +653,6 @@ class TestGetBudgetWindowEconomicImpact: def economy_service( self, mock_country_package_versions, - mock_get_dataset_version, mock_policy_service, mock_logger, mock_datetime, @@ -1170,7 +1104,6 @@ def test__given_runtime_cache_version__uses_versioned_cache_key_for_budget_windo economy_service, base_params, mock_country_package_versions, - mock_get_dataset_version, mock_simulation_api, mock_budget_window_cache, mock_logger, @@ -1723,7 +1656,8 @@ class TestSetupSimOptions: """Tests for _setup_sim_options method. Note: _setup_sim_options now expects pre-normalized regions and returns - GCS paths in the data field (not None). + no concrete data field for default datasets. The simulation gateway + resolves the certified dataset from the requested .py bundle. """ test_country_id = "us" @@ -1755,9 +1689,7 @@ def test__given_us_nationwide__returns_correct_sim_options(self): ) assert sim_options["time_period"] == self.test_time_period assert sim_options["region"] == "us" - assert ( - sim_options["data"] == "gs://policyengine-us-data/enhanced_cps_2024.h5" - ) + assert sim_options["data"] is None def test__given_us_state_ca__returns_correct_sim_options(self): # Test with a normalized US state (prefixed format) @@ -1785,7 +1717,7 @@ def test__given_us_state_ca__returns_correct_sim_options(self): assert sim_options["baseline"] == json.loads(current_law_baseline_policy) assert sim_options["time_period"] == time_period assert sim_options["region"] == "state/ca" - assert sim_options["data"] == "gs://policyengine-us-data/states/CA.h5" + assert sim_options["data"] is None def test__given_us_state_utah__returns_correct_sim_options(self): # Test with normalized Utah state @@ -1813,7 +1745,7 @@ def test__given_us_state_utah__returns_correct_sim_options(self): assert sim_options["baseline"] == json.loads(current_law_baseline_policy) assert sim_options["time_period"] == time_period assert sim_options["region"] == "state/ut" - assert sim_options["data"] == "gs://policyengine-us-data/states/UT.h5" + assert sim_options["data"] is None def test__given_cliff_target__returns_correct_sim_options(self): country_id = "us" @@ -1842,9 +1774,7 @@ def test__given_cliff_target__returns_correct_sim_options(self): assert sim_options["baseline"] == json.loads(current_law_baseline_policy) assert sim_options["time_period"] == time_period assert sim_options["region"] == region - assert ( - sim_options["data"] == "gs://policyengine-us-data/enhanced_cps_2024.h5" - ) + assert sim_options["data"] is None assert sim_options["include_cliffs"] is True def test__given_uk__returns_correct_sim_options(self): @@ -1869,10 +1799,7 @@ def test__given_uk__returns_correct_sim_options(self): sim_options = sim_options_model.model_dump() assert sim_options["country"] == country_id assert sim_options["region"] == region - assert ( - sim_options["data"] - == "gs://policyengine-uk-data-private/enhanced_frs_2023_24.h5" - ) + assert sim_options["data"] is None def test__given_congressional_district__returns_correct_sim_options( self, @@ -1897,7 +1824,7 @@ def test__given_congressional_district__returns_correct_sim_options( sim_options = sim_options_model.model_dump() assert sim_options["region"] == "congressional_district/CA-37" - assert sim_options["data"] == "gs://policyengine-us-data/districts/CA-37.h5" + assert sim_options["data"] is None def test__given_explicit_dataset__returns_named_dataset(self): service = EconomyService() @@ -1913,10 +1840,7 @@ def test__given_explicit_dataset__returns_named_dataset(self): ) sim_options = sim_options_model.model_dump() - assert ( - sim_options["data"] - == "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.77.0" - ) + assert sim_options["data"] == "enhanced_cps" class TestSetupRegion: """Tests for _setup_region method. @@ -2016,55 +1940,47 @@ def test__given_invalid_place_fips__raises_value_error(self): class TestSetupData: """Tests for _setup_data method. - Note: _setup_data now uses get_default_dataset from policyengine package - to return GCS paths for all region types (not None). + Default requests omit a concrete dataset so the simulation gateway can + resolve the certified dataset from the requested .py bundle. Explicit + dataset values are passed through as legacy overrides. """ - def test__given_us_place__returns_state_dataset(self): - # Test with place region - uses parent state's dataset + def test__given_us_place_default__omits_data(self): service = EconomyService() result = service._setup_data("us", "place/NJ-57000") - assert result == "gs://policyengine-us-data/states/NJ.h5" + assert result is None - def test__given_us_state_ca__returns_state_dataset(self): - # Test with US state - returns state-specific dataset + def test__given_us_state_ca_default__omits_data(self): service = EconomyService() result = service._setup_data("us", "state/ca") - assert result == "gs://policyengine-us-data/states/CA.h5" + assert result is None - def test__given_us_state_ut__returns_state_dataset(self): - # Test with Utah state - returns state-specific dataset + def test__given_us_state_ut_default__omits_data(self): service = EconomyService() result = service._setup_data("us", "state/ut") - assert result == "gs://policyengine-us-data/states/UT.h5" + assert result is None - def test__given_us_nationwide__returns_cps_dataset(self): - # Test with US nationwide region + def test__given_us_nationwide_default__omits_data(self): service = EconomyService() result = service._setup_data("us", "us") - assert result == "gs://policyengine-us-data/enhanced_cps_2024.h5" + assert result is None - def test__given_congressional_district__returns_district_dataset(self): - # Test with congressional district - returns district-specific dataset + def test__given_congressional_district_default__omits_data(self): service = EconomyService() result = service._setup_data("us", "congressional_district/CA-37") - assert result == "gs://policyengine-us-data/districts/CA-37.h5" + assert result is None - def test__given_uk__returns_efrs_dataset(self): - # Test with UK - returns enhanced FRS dataset + def test__given_uk_default__omits_data(self): service = EconomyService() result = service._setup_data("uk", "uk") - assert result == "gs://policyengine-uk-data-private/enhanced_frs_2023_24.h5" + assert result is None - def test__given_invalid_country__raises_value_error(self, mock_logger): - # Test with invalid country + def test__given_invalid_country_default__omits_data(self, mock_logger): service = EconomyService() - with pytest.raises(ValueError) as exc_info: - service._setup_data("invalid", "region") - assert "invalid" in str(exc_info.value).lower() + result = service._setup_data("invalid", "region") + assert result is None def test__given_passthrough_dataset__returns_dataset_directly(self): - # Test with passthrough dataset (national-with-breakdowns) service = EconomyService() result = service._setup_data("us", "us", dataset="national-with-breakdowns") assert result == "national-with-breakdowns" @@ -2082,35 +1998,27 @@ def test__given_passthrough_test_dataset__returns_dataset_directly( def test__given_explicit_us_enhanced_cps__returns_named_dataset(self): service = EconomyService() result = service._setup_data("us", "us", dataset="enhanced_cps") - assert ( - result - == "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.77.0" - ) + assert result == "enhanced_cps" def test__given_explicit_us_cps__returns_named_dataset(self): service = EconomyService() result = service._setup_data("us", "us", dataset="cps") - assert result == "hf://policyengine/policyengine-us-data/cps_2023.h5@1.77.0" + assert result == "cps" def test__given_explicit_uk_enhanced_frs__returns_named_dataset(self): service = EconomyService() result = service._setup_data("uk", "uk", dataset="enhanced_frs") - assert ( - result - == "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2023_24.h5@1.40.3" - ) + assert result == "enhanced_frs" - def test__given_default_dataset__uses_get_default_dataset(self): - # Test that "default" falls through to get_default_dataset + def test__given_default_dataset__omits_data(self): service = EconomyService() result = service._setup_data("us", "state/ca", dataset="default") - assert result == "gs://policyengine-us-data/states/CA.h5" + assert result is None - def test__given_unknown_dataset__uses_get_default_dataset(self): - # Test that unknown dataset values fall through to get_default_dataset + def test__given_unknown_dataset__passes_through_legacy_designator(self): service = EconomyService() result = service._setup_data("us", "state/ca", dataset="unknown-dataset") - assert result == "gs://policyengine-us-data/states/CA.h5" + assert result == "unknown-dataset" class TestValidateUsRegion: """Tests for the _validate_us_region method.""" diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index fcfeaa075..07489f24e 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -1,7 +1,9 @@ from __future__ import annotations +import json import os import re +import shlex import subprocess from pathlib import Path @@ -67,6 +69,25 @@ def _run_script(path: str, env: dict[str, str]) -> subprocess.CompletedProcess[s ) +def _run_simulation_version_guard( + versions_response: dict, *args: str +) -> subprocess.CompletedProcess[str]: + versions_json = json.dumps(versions_response) + command = ( + "curl() { printf '%s' " + f"{shlex.quote(versions_json)}" + '; }; . .github/request-simulation-model-versions.sh "$@"' + ) + return subprocess.run( + ["bash", "-c", command, "request-simulation-model-versions.sh", *args], + cwd=REPO, + env=_script_env(SIMULATION_API_URL="https://simulation.example.test"), + text=True, + capture_output=True, + check=False, + ) + + def _push_workflow() -> str: return (REPO / ".github/workflows/push.yml").read_text(encoding="utf-8") @@ -133,6 +154,57 @@ def test_cloud_run_startup_script_is_shell_syntax_valid(): assert result.returncode == 0, result.stderr +def test_simulation_version_guard_accepts_bundle_and_compatible_country_routes(): + result = _run_simulation_version_guard( + { + "policyengine": { + "latest": "4.18.3", + "4.18.3": "policyengine-simulation-py4-18-3", + }, + "us": { + "latest": "1.729.0", + "1.729.0": "policyengine-simulation-py4-18-3", + }, + "uk": { + "latest": "2.89.2", + "2.89.2": "policyengine-simulation-py4-18-3", + }, + }, + "-py", + "4.18.3", + "-us", + "1.729.0", + "-uk", + "2.89.2", + ) + + assert result.returncode == 0, result.stderr + assert "SUCCESS: PolicyEngine bundle route is deployed and ready" in result.stdout + + +def test_simulation_version_guard_rejects_country_route_to_different_app(): + result = _run_simulation_version_guard( + { + "policyengine": { + "latest": "4.18.3", + "4.18.3": "policyengine-simulation-py4-18-3", + }, + "us": { + "latest": "1.729.0", + "1.729.0": "policyengine-simulation-us1-729-0", + }, + }, + "-py", + "4.18.3", + "-us", + "1.729.0", + ) + + assert result.returncode == 1 + assert "resolves to policyengine-simulation-us1-729-0" in result.stdout + assert "not bundle app policyengine-simulation-py4-18-3" in result.stdout + + def test_cloud_run_dockerfile_runs_startup_with_bash(): dockerfile = (REPO / "gcp/cloud_run/Dockerfile").read_text(encoding="utf-8") diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py index 2cb9503b1..b6d5451e5 100644 --- a/tests/unit/test_constants.py +++ b/tests/unit/test_constants.py @@ -1,7 +1,18 @@ +import json +import subprocess +import sys + +import pytest + from policyengine_api.constants import ( + COUNTRY_PACKAGE_VERSIONS, + POLICYENGINE_CORE_VERSION, + POLICYENGINE_VERSION, + REGION_PREFIXES, UK_REGION_TYPES, US_REGION_TYPES, - REGION_PREFIXES, + get_py_manifest, + _load_policyengine_bundle, _normalize_distribution_name, _resolve_distribution_version, ) @@ -112,3 +123,89 @@ def test__resolve_distribution_version_falls_back_to_default(self): _resolve_distribution_version({}, "policyengine-core", "policyengine") == "0.0.0" ) + + +class TestPolicyEngineBundleVersions: + def test__get_py_manifest_returns_packaged_manifest_path(self): + manifest_path = get_py_manifest() + + assert manifest_path.name == "manifest.json" + assert manifest_path.exists() + + def test__constants_load_from_manifest_without_importing_policyengine(self): + code = """ +import importlib.abc +import json +import sys + + +class BlockPolicyEngineImports(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "policyengine" or fullname.startswith("policyengine."): + raise AssertionError(f"Unexpected import: {fullname}") + return None + + +sys.meta_path.insert(0, BlockPolicyEngineImports()) + +from policyengine_api.constants import ( # noqa: E402 + COUNTRY_PACKAGE_VERSIONS, + POLICYENGINE_CORE_VERSION, + POLICYENGINE_VERSION, +) + +print( + json.dumps( + { + "policyengine": POLICYENGINE_VERSION, + "core": POLICYENGINE_CORE_VERSION, + "us": COUNTRY_PACKAGE_VERSIONS["us"], + "uk": COUNTRY_PACKAGE_VERSIONS["uk"], + } + ) +) +""" + result = subprocess.run( + [sys.executable, "-c", code], + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "policyengine": "4.18.3", + "core": "3.27.1", + "us": "1.729.0", + "uk": "2.89.2", + } + + def test__load_policyengine_bundle_rejects_missing_manifest( + self, monkeypatch, tmp_path + ): + monkeypatch.setattr( + "policyengine_api.constants.get_py_manifest", + lambda: tmp_path / "missing-manifest.json", + ) + + with pytest.raises(RuntimeError, match="Could not read PolicyEngine"): + _load_policyengine_bundle() + + def test__load_policyengine_bundle_rejects_non_object_manifest( + self, monkeypatch, tmp_path + ): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text("[]", encoding="utf-8") + monkeypatch.setattr( + "policyengine_api.constants.get_py_manifest", + lambda: manifest_path, + ) + + with pytest.raises(RuntimeError, match="must be a JSON object"): + _load_policyengine_bundle() + + def test__uses_policyengine_bundle_versions_for_us_uk_and_core(self): + assert POLICYENGINE_VERSION == "4.18.3" + assert POLICYENGINE_CORE_VERSION == "3.27.1" + assert COUNTRY_PACKAGE_VERSIONS["us"] == "1.729.0" + assert COUNTRY_PACKAGE_VERSIONS["uk"] == "2.89.2" diff --git a/uv.lock b/uv.lock index 480fe985f..404fd45ed 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,15 @@ resolution-markers = [ "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[[package]] +name = "a2wsgi" +version = "1.10.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/cb/822c56fbea97e9eee201a2e434a80437f6750ebcb1ed307ee3a0a7505b14/a2wsgi-1.10.10.tar.gz", hash = "sha256:a5bcffb52081ba39df0d5e9a884fc6f819d92e3a42389343ba77cbf809fe1f45", size = 18799, upload-time = "2025-06-18T09:00:10.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/d5/349aba3dc421e73cbd4958c0ce0a4f1aa3a738bc0d7de75d2f40ed43a535/a2wsgi-1.10.10-py3-none-any.whl", hash = "sha256:d2b21379479718539dc15fce53b876251a0efe7615352dfe49f6ad1bc507848d", size = 17389, upload-time = "2025-06-18T09:00:09.676Z" }, +] + [[package]] name = "aiofiles" version = "25.1.0" @@ -378,15 +387,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, ] -[[package]] -name = "caugetch" -version = "0.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/ec/519cb37e3e58e23a5b02a74049128f6e701ccd8892b0cebecf701fac6177/caugetch-0.0.1.tar.gz", hash = "sha256:6f6ddb3b928fa272071b02aabb3342941cd99992f27413ba8c189eb4dc3e33b0", size = 2071, upload-time = "2019-10-15T22:39:49.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/33/64fee4626ec943c2d0c4eee31c784dab8452dfe014916190730880d4ea62/caugetch-0.0.1-py3-none-any.whl", hash = "sha256:ee743dcbb513409cd24cfc42435418073683ba2f4bb7ee9f8440088a47d59277", size = 3439, upload-time = "2019-10-15T22:39:47.122Z" }, -] - [[package]] name = "census" version = "0.8.26" @@ -579,15 +579,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] -[[package]] -name = "clipboard" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyperclip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/38/17f3885713d0f39994563029942b1d31c93d4e56d80da505abfbfb3a3bc4/clipboard-0.0.4.tar.gz", hash = "sha256:a72a78e9c9bf68da1c3f29ee022417d13ec9e3824b511559fd2b702b1dd5b817", size = 1713, upload-time = "2014-05-22T12:49:08.683Z" } - [[package]] name = "cloud-sql-python-connector" version = "1.20.2" @@ -812,6 +803,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/6f/5eaf3e249c636e616ebb52e369a4a2f1d32b1caf9a611b4f917b3dd21423/faiss_cpu-1.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c", size = 8556374, upload-time = "2025-12-24T10:27:36.653Z" }, ] +[[package]] +name = "fastapi" +version = "0.138.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/a9/9f8f7e00195c29836e9bf58bbbaf579e29878b8a67851efff93d9b6d4eb7/fastapi-0.138.2.tar.gz", hash = "sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8", size = 420423, upload-time = "2026-06-29T12:44:12.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" }, +] + [[package]] name = "filelock" version = "3.29.0" @@ -978,21 +985,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] -[[package]] -name = "getpass4" -version = "0.0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "caugetch" }, - { name = "clipboard" }, - { name = "colorama" }, - { name = "pyperclip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/f9/312f84afc384f693d02eb4ff7306a7268577a8b808aa08f0124c9abba683/getpass4-0.0.14.1.tar.gz", hash = "sha256:80aa4e3a665f2eccc6cda3ee22125eeb5c6338e91c40c4fd010b3c94c7aa4d3a", size = 5078, upload-time = "2021-11-28T17:08:47.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/d3/ea114aba31f76418b2162e811793cde2e822c9d9ea8ca98d67f9e1f1bde6/getpass4-0.0.14.1-py3-none-any.whl", hash = "sha256:6642c11fb99db1bec90b963e863ec71cdb0b8888000f5089c6377bfbf833f8a9", size = 8683, upload-time = "2021-11-28T17:08:45.468Z" }, -] - [[package]] name = "gitdb" version = "4.0.12" @@ -1217,9 +1209,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" }, { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, - { url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" }, { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, @@ -1227,9 +1217,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, @@ -1237,9 +1225,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, @@ -1247,9 +1233,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, - { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, @@ -1257,9 +1241,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, @@ -2606,30 +2588,44 @@ wheels = [ [[package]] name = "policyengine" -version = "0.13.0" +version = "4.18.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "diskcache" }, - { name = "getpass4" }, { name = "google-cloud-storage" }, + { name = "h5py" }, + { name = "jsonschema" }, { name = "microdf-python" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/13/0dd4f39d7ec5d14add10c08e4d61aad296831802edae526e1907fc023aec/policyengine-4.18.3.tar.gz", hash = "sha256:ed95cfea02770e62bc0945a9e88bcbc96c94bf1fc717162c194b33948564a089", size = 697659, upload-time = "2026-06-25T19:05:19.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b9/b3651013363f71c90795c7fd11ae7eed2b9acb2bafeb743260b972f2d58b/policyengine-4.18.3-py3-none-any.whl", hash = "sha256:fe2adbb576c08acb874ad98b5b03b8c41c38f654cd41ad6ada14a32427774aa0", size = 211031, upload-time = "2026-06-25T19:05:17.906Z" }, +] + +[package.optional-dependencies] +models = [ { name = "policyengine-core" }, { name = "policyengine-uk" }, { name = "policyengine-us" }, - { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/f3/eeea7dab690e46cd91533691eef41097f1c2e9eb729d4f70408b865c750e/policyengine-0.13.0.tar.gz", hash = "sha256:15cf9f0ff0801c8cf12fbaeabe5e03e3cc2822cd3436b08553cf2ef0e00673ba", size = 230501, upload-time = "2026-04-08T13:41:59.62Z" } [[package]] name = "policyengine-api" -version = "3.40.11" +version = "3.43.1" source = { editable = "." } dependencies = [ + { name = "a2wsgi" }, { name = "anthropic" }, { name = "assertpy" }, { name = "click" }, { name = "cloud-sql-python-connector" }, { name = "faiss-cpu" }, + { name = "fastapi" }, { name = "flask" }, { name = "flask-caching" }, { name = "flask-cors" }, @@ -2640,13 +2636,10 @@ dependencies = [ { name = "markupsafe" }, { name = "microdf-python" }, { name = "openai" }, - { name = "policyengine" }, + { name = "policyengine", extra = ["models"] }, { name = "policyengine-canada" }, - { name = "policyengine-core" }, { name = "policyengine-il" }, { name = "policyengine-ng" }, - { name = "policyengine-uk" }, - { name = "policyengine-us" }, { name = "pydantic" }, { name = "pymysql" }, { name = "python-dotenv" }, @@ -2654,6 +2647,7 @@ dependencies = [ { name = "rq" }, { name = "sqlalchemy" }, { name = "streamlit" }, + { name = "uvicorn", extra = ["standard"] }, { name = "werkzeug" }, ] @@ -2670,6 +2664,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "a2wsgi", specifier = ">=1.10,<2" }, { name = "anthropic" }, { name = "assertpy" }, { name = "build", marker = "extra == 'dev'" }, @@ -2677,6 +2672,7 @@ requires-dist = [ { name = "cloud-sql-python-connector" }, { name = "coverage", marker = "extra == 'dev'" }, { name = "faiss-cpu" }, + { name = "fastapi", specifier = ">=0.115,<1" }, { name = "flask", specifier = ">=3,<4" }, { name = "flask-caching", specifier = ">=2,<3" }, { name = "flask-cors", specifier = ">=5,<6" }, @@ -2687,13 +2683,10 @@ requires-dist = [ { name = "markupsafe", specifier = ">=3,<4" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "openai" }, - { name = "policyengine", specifier = ">0.12.0,<1" }, + { name = "policyengine", extras = ["models"], specifier = "==4.18.3" }, { name = "policyengine-canada", specifier = "==0.96.3" }, - { name = "policyengine-core", specifier = ">=3.23.5" }, { name = "policyengine-il", specifier = "==0.1.0" }, { name = "policyengine-ng", specifier = "==0.5.1" }, - { name = "policyengine-uk", specifier = "==2.88.0" }, - { name = "policyengine-us", specifier = "==1.653.3" }, { name = "pydantic" }, { name = "pymysql" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -2706,6 +2699,7 @@ requires-dist = [ { name = "sqlalchemy", specifier = ">=2,<3" }, { name = "streamlit" }, { name = "towncrier", marker = "extra == 'dev'", specifier = ">=24.8.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.32,<1" }, { name = "werkzeug" }, ] provides-extras = ["dev"] @@ -2740,7 +2734,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.25.3" +version = "3.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -2760,9 +2754,9 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/a6/46a316ef534adbedffbdfb8b2b9cfc89be572e3d75fa79c61103c771000e/policyengine_core-3.25.3.tar.gz", hash = "sha256:bf6a22cc49eeeaba310531321cb932c41a2f10c6a5f4cc20fd7677641f60055d", size = 466467, upload-time = "2026-04-28T00:36:10.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/28/cbc23d0c61d431cbfdbaea3f7a71b2230187ab2e57f1430a551598b39515/policyengine_core-3.27.1.tar.gz", hash = "sha256:21471e3f6e95b8d5c00babcb5a5d363fdc1cd4b02c338e5eb4c9da18e08b6a10", size = 484853, upload-time = "2026-06-11T18:18:36.753Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/e1/d3451e5c279bcea5da49c5cab3b3eec9e7fa35aafd62289394b769619b7e/policyengine_core-3.25.3-py3-none-any.whl", hash = "sha256:5b11ef29db4275121b58664a9c5ebd6478eeff5001e9f55b71e13716bbd9085f", size = 231186, upload-time = "2026-04-28T00:36:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/81/098994e62401e9ce0d799e3f01329ba4a8792599d17ae0ef67fff1ddd3ff/policyengine_core-3.27.1-py3-none-any.whl", hash = "sha256:dac7928b502baa56fd22956f089689faa4d7e04a21aab7f1f29b34961d684ef8", size = 238480, upload-time = "2026-06-11T18:18:35.203Z" }, ] [[package]] @@ -2793,7 +2787,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.88.0" +version = "2.89.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2801,14 +2795,14 @@ dependencies = [ { name = "pydantic" }, { name = "tables" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/cf/749dea25c17210b5dc40098363e0b6a60b7fc5feb69ff77c74b88deb5cde/policyengine_uk-2.88.0.tar.gz", hash = "sha256:d157c7336b7aa3a321f317af1a4f111d7b857451ff43f4998abdc5a8c893e989", size = 1166666, upload-time = "2026-04-17T19:22:55.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/bc/d9cadc5b91804dab0937506e02463a4146a4c996b3d6cc400599b688eb7a/policyengine_uk-2.89.2.tar.gz", hash = "sha256:9eefdc321799f1b610dc1d72b465b6d35a0595469d67c2e4445529c3063a6ef7", size = 1217538, upload-time = "2026-06-18T10:09:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/7e/8a2a42eac1da63730a865964aa17e7fd4420ce4db4c80001c1b5ca6011e8/policyengine_uk-2.88.0-py3-none-any.whl", hash = "sha256:46a3ba443b43ec810c5efaccd4645edb63c8dc90ef5acf9b0cdf5ace86b9334d", size = 1867764, upload-time = "2026-04-17T19:22:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/db/ce3154ba69b6fcd1e9e922ceee705ef4ddb1f81553da1e63b9296e74a4dc/policyengine_uk-2.89.2-py3-none-any.whl", hash = "sha256:80965d3dd7dc767db9b083820d40262ce543020d5a8880a0cf88da10ae641b24", size = 2001007, upload-time = "2026-06-18T10:09:44.808Z" }, ] [[package]] name = "policyengine-us" -version = "1.653.3" +version = "1.729.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2818,9 +2812,9 @@ dependencies = [ { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/60/5b736fa238559857fbf29168933c809eaada9abf006d26910b7958f5748e/policyengine_us-1.653.3.tar.gz", hash = "sha256:8a5c33997b7aefa2061d0dafce837b130e8ebdb0b9f83ae8c236f80cbf1805d6", size = 9180339, upload-time = "2026-04-18T12:06:45.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/cb/b2efba2094a708cd71890d98d72b99394fabc5894a4cceec14381e03fa35/policyengine_us-1.729.0.tar.gz", hash = "sha256:ac05c4d621c7f848b0806effc14e913160d5d47d777eadced6bc18edf392d75c", size = 10373862, upload-time = "2026-06-14T18:05:25.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/07/25f39a2bfa1ff210cd8e78826c47c03b9040a98a83f4eed59c434c1ed862/policyengine_us-1.653.3-py3-none-any.whl", hash = "sha256:67a49b98d85c060b24d547a569e91a6703c0fc9c41299c1c67f4ecfac75c67c6", size = 9445650, upload-time = "2026-04-18T12:06:43.163Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7d/778f92ae94997b00c3c9ac34b345f6c9333435f905670ee4eeb2f5e19809/policyengine_us-1.729.0-py3-none-any.whl", hash = "sha256:8d21d3f7c0e82a9415edffe8ea53939330a63d9c8f6bd334299bddb697cf2c00", size = 11905076, upload-time = "2026-06-14T18:05:21.806Z" }, ] [[package]] @@ -3231,15 +3225,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] -[[package]] -name = "pyperclip" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, -] - [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -3991,6 +3976,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -4009,6 +4043,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0"