diff --git a/.github/scripts/deploy-cloud-run-simulation-entry.sh b/.github/scripts/deploy-cloud-run-simulation-entry.sh index 92dafe4bb..e33f41a59 100755 --- a/.github/scripts/deploy-cloud-run-simulation-entry.sh +++ b/.github/scripts/deploy-cloud-run-simulation-entry.sh @@ -77,6 +77,10 @@ required_variables=( OLD_GATEWAY_AUTH_AUDIENCE_VALUE OLD_GATEWAY_AUTH_CLIENT_ID_VALUE OLD_GATEWAY_AUTH_CLIENT_SECRET_SECRET_NAME + OBSERVABILITY_SERVICE_NAMESPACE + OBSERVABILITY_TRACE_PROJECT_ID + OTEL_EXPORTER_OTLP_ENDPOINT + POLICYENGINE_OTEL_GOOGLE_AUDIENCE RUNNER_TEMP ) for variable_name in "${required_variables[@]}"; do @@ -148,6 +152,14 @@ jq -n ' OLD_GATEWAY_AUTH_ISSUER: env.OLD_GATEWAY_AUTH_ISSUER_VALUE, OLD_GATEWAY_AUTH_AUDIENCE: env.OLD_GATEWAY_AUTH_AUDIENCE_VALUE, OLD_GATEWAY_AUTH_CLIENT_ID: env.OLD_GATEWAY_AUTH_CLIENT_ID_VALUE, + OBSERVABILITY_SERVICE_NAMESPACE: env.OBSERVABILITY_SERVICE_NAMESPACE, + OBSERVABILITY_TRACE_PROJECT_ID: env.OBSERVABILITY_TRACE_PROJECT_ID, + OTEL_EXPORTER_OTLP_ENDPOINT: env.OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_PROTOCOL: "grpc", + OTEL_TRACES_EXPORTER: "otlp", + OTEL_METRICS_EXPORTER: "otlp", + OTEL_TRACES_SAMPLER_ARG: "1.0", + POLICYENGINE_OTEL_GOOGLE_AUDIENCE: env.POLICYENGINE_OTEL_GOOGLE_AUDIENCE, STAGE12_ENABLED: ( if env.DEPLOY_STAGE12_V2 == "true" then env.STAGE12_ENABLED_VALUE else "0" end ) diff --git a/.github/scripts/modal-sync-secrets.sh b/.github/scripts/modal-sync-secrets.sh index 1ac940d67..474b686ef 100755 --- a/.github/scripts/modal-sync-secrets.sh +++ b/.github/scripts/modal-sync-secrets.sh @@ -1,7 +1,7 @@ #!/bin/bash # Sync secrets from GitHub to Modal environment # Usage: ./modal-sync-secrets.sh -# Required env vars: LOGFIRE_TOKEN, HF_TOKEN +# Required env vars: HF_TOKEN # Optional env vars: GCP_CREDENTIALS_JSON set -euo pipefail @@ -54,13 +54,6 @@ if truthy "${GATEWAY_AUTH_REQUIRED:-}" && [ ${#missing[@]} -gt 0 ]; then exit 1 fi -# Sync Logfire secret -uv run modal secret create policyengine-logfire \ - "LOGFIRE_TOKEN=${LOGFIRE_TOKEN:-}" \ - "LOGFIRE_ENVIRONMENT=$GH_ENV" \ - --env="$MODAL_ENV" \ - --force || true - # Sync GCP credentials if provided if [ -n "${GCP_CREDENTIALS_JSON:-}" ]; then uv run modal secret create gcp-credentials \ diff --git a/.github/workflows/simulation-deploy.reusable.yml b/.github/workflows/simulation-deploy.reusable.yml index 0c63a1a65..127435d4f 100644 --- a/.github/workflows/simulation-deploy.reusable.yml +++ b/.github/workflows/simulation-deploy.reusable.yml @@ -127,7 +127,6 @@ jobs: env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} - LOGFIRE_TOKEN: ${{ secrets.LOGFIRE_TOKEN }} HF_TOKEN: ${{ secrets.HF_TOKEN }} GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} @@ -222,6 +221,10 @@ jobs: env: IMAGE: ${{ steps.image.outputs.uri }} TAG: ${{ steps.metadata.outputs.tag }} + OBSERVABILITY_SERVICE_NAMESPACE: ${{ vars.OBSERVABILITY_SERVICE_NAMESPACE }} + OBSERVABILITY_TRACE_PROJECT_ID: ${{ vars.OBSERVABILITY_TRACE_PROJECT_ID }} + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OBSERVABILITY_OTLP_ENDPOINT }} + POLICYENGINE_OTEL_GOOGLE_AUDIENCE: ${{ vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE }} DEPLOY_STAGE12_V2: ${{ inputs.deploy_stage12_v2 }} APP_ENVIRONMENT: ${{ inputs.deployment_environment }} MODAL_ENVIRONMENT: ${{ inputs.modal_environment }} @@ -292,7 +295,7 @@ jobs: deploy_gateway: name: Deploy and unit test Modal gateway if: ${{ inputs.deploy_existing_stack }} - needs: prepare + needs: [prepare, deploy_executor] runs-on: ubuntu-latest environment: ${{ inputs.release_environment }} outputs: @@ -315,6 +318,14 @@ jobs: env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} + OBSERVABILITY_SERVICE_NAMESPACE: ${{ vars.OBSERVABILITY_SERVICE_NAMESPACE }} + OBSERVABILITY_TRACE_PROJECT_ID: ${{ vars.OBSERVABILITY_TRACE_PROJECT_ID }} + OBSERVABILITY_LOGGING_PROJECT_ID: ${{ vars.OBSERVABILITY_LOGGING_PROJECT_ID }} + OBSERVABILITY_LOG_NAME: ${{ vars.OBSERVABILITY_LOG_NAME }} + OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ vars.OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL }} + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OBSERVABILITY_OTLP_ENDPOINT }} + POLICYENGINE_OTEL_GOOGLE_AUDIENCE: ${{ vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE }} run: uv run modal deploy --env="${{ inputs.modal_environment }}" src/policyengine_simulation_gateway/app.py - name: Run gateway unit tests @@ -383,6 +394,14 @@ jobs: POLICYENGINE_MANIFEST_DIGEST: ${{ steps.precompute.outputs.manifest_digest }} POLICYENGINE_ARTIFACT_BUCKET: ${{ vars.POLICYENGINE_ARTIFACT_BUCKET }} GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }} + OBSERVABILITY_SERVICE_NAMESPACE: ${{ vars.OBSERVABILITY_SERVICE_NAMESPACE }} + OBSERVABILITY_TRACE_PROJECT_ID: ${{ vars.OBSERVABILITY_TRACE_PROJECT_ID }} + OBSERVABILITY_LOGGING_PROJECT_ID: ${{ vars.OBSERVABILITY_LOGGING_PROJECT_ID }} + OBSERVABILITY_LOG_NAME: ${{ vars.OBSERVABILITY_LOG_NAME }} + OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ vars.OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL }} + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OBSERVABILITY_OTLP_ENDPOINT }} + POLICYENGINE_OTEL_GOOGLE_AUDIENCE: ${{ vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE }} run: uv run modal deploy --env="${{ inputs.modal_environment }}" src/modal/app.py - name: Run executor unit tests @@ -543,6 +562,14 @@ jobs: STAGE12_EXPECT_US_VERSION: ${{ needs.prepare.outputs.us_version }} STAGE12_EXPECT_UK_VERSION: ${{ needs.prepare.outputs.uk_version }} STAGE12_ARTIFACT_BUCKET: ${{ vars.STAGE12_ARTIFACT_BUCKET }} + OBSERVABILITY_SERVICE_NAMESPACE: ${{ vars.OBSERVABILITY_SERVICE_NAMESPACE }} + OBSERVABILITY_TRACE_PROJECT_ID: ${{ vars.OBSERVABILITY_TRACE_PROJECT_ID }} + OBSERVABILITY_LOGGING_PROJECT_ID: ${{ vars.OBSERVABILITY_LOGGING_PROJECT_ID }} + OBSERVABILITY_LOG_NAME: ${{ vars.OBSERVABILITY_LOG_NAME }} + OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ vars.OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL }} + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OBSERVABILITY_OTLP_ENDPOINT }} + POLICYENGINE_OTEL_GOOGLE_AUDIENCE: ${{ vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE }} run: uv run modal deploy --env="${{ inputs.modal_environment }}" src/modal/v2_app.py - name: Validate installed bundles, secrets, datasets, and calculations diff --git a/libs/policyengine-fastapi/pyproject.toml b/libs/policyengine-fastapi/pyproject.toml index 7b875ccad..92b985d0b 100644 --- a/libs/policyengine-fastapi/pyproject.toml +++ b/libs/policyengine-fastapi/pyproject.toml @@ -10,14 +10,14 @@ requires-python = ">=3.13" dependencies = [ "fastapi[standard] >=0.115.8,<0.116.0", "pyjwt >=2.10.1,<3.0.0", - "opentelemetry-sdk >=1.30.0,<2.0.0", + "opentelemetry-sdk >=1.44.0,<2.0.0", "sqlmodel >=0.0.22,<0.0.23", "python-json-logger >=3.2.1,<4.0.0", - "opentelemetry-instrumentation-logging >=0.51b0,<0.52", + "opentelemetry-instrumentation-logging >=0.65b0,<0.66", "opentelemetry-exporter-gcp-trace >=1.9.0,<2.0.0", "opentelemetry-exporter-gcp-monitoring >=1.9.0a0,<2.0.0", - "opentelemetry-instrumentation-fastapi >=0.51b0,<0.52", - "opentelemetry-instrumentation-sqlalchemy>=0.51b0", + "opentelemetry-instrumentation-fastapi >=0.65b0,<0.66", + "opentelemetry-instrumentation-sqlalchemy>=0.65b0,<0.66", "uvicorn>=0.35.0", ] diff --git a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/__init__.py b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/__init__.py index 7061d3291..c26d4e1fc 100644 --- a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/__init__.py +++ b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/__init__.py @@ -15,13 +15,12 @@ SimulationCompositeTraceResponse as SimulationCompositeTraceResponse, SimulationLifecycleEvent as SimulationLifecycleEvent, SimulationRunSummary as SimulationRunSummary, - SimulationTelemetryEnvelope as SimulationTelemetryEnvelope, SimulationTimelineEntry as SimulationTimelineEntry, TracerArtifactManifest as TracerArtifactManifest, VersionStageMetricResponse as VersionStageMetricResponse, ) from .correlation import ( - generate_run_id as generate_run_id, + generate_observability_id as generate_observability_id, stable_config_hash as stable_config_hash, ) from .emitters import ( diff --git a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/contracts.py b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/contracts.py index 5aaae615d..12c888709 100644 --- a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/contracts.py +++ b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/contracts.py @@ -13,8 +13,8 @@ class ObservabilityModel(BaseModel): class CorrelatedRunFields(ObservabilityModel): - run_id: str - process_id: str | None = None + observability_id: str + submission_claim_id: str | None = None job_id: str | None = None trace_id: str | None = None request_id: str | None = None @@ -54,19 +54,6 @@ class TracerArtifactManifest(CorrelatedRunFields): generated_at: datetime -class SimulationTelemetryEnvelope(ObservabilityModel): - run_id: str - process_id: str | None = None - request_id: str | None = None - traceparent: str | None = None - requested_at: datetime | None = None - simulation_kind: str | None = None - geography_code: str | None = None - geography_type: str | None = None - config_hash: str | None = None - capture_mode: TracerCaptureMode = TracerCaptureMode.DISABLED - - class SimulationRunSummary(CorrelatedRunFields): status: str requested_at: datetime | None = None diff --git a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/correlation.py b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/correlation.py index 05d419de6..9a2dc05e3 100644 --- a/libs/policyengine-fastapi/src/policyengine_fastapi/observability/correlation.py +++ b/libs/policyengine-fastapi/src/policyengine_fastapi/observability/correlation.py @@ -6,7 +6,7 @@ from uuid import uuid4 -def generate_run_id() -> str: +def generate_observability_id() -> str: return str(uuid4()) diff --git a/libs/policyengine-fastapi/tests/test_observability.py b/libs/policyengine-fastapi/tests/test_observability.py index 9323ecdc2..e40a35856 100644 --- a/libs/policyengine-fastapi/tests/test_observability.py +++ b/libs/policyengine-fastapi/tests/test_observability.py @@ -7,13 +7,12 @@ SimulationLifecycleEvent, SimulationRunSummary, SimulationStage, - SimulationTelemetryEnvelope, SimulationTimelineEntry, TracerArtifactManifest, TracerCaptureMode, VersionStageMetricResponse, build_observability, - generate_run_id, + generate_observability_id, get_observability, parse_header_value_pairs, stable_config_hash, @@ -49,11 +48,11 @@ def test_observability_config_disabled__returns_disabled_defaults(): def test_correlation_helpers__generate_ids_and_stable_hashes(): - run_id = generate_run_id() + observability_id = generate_observability_id() left = stable_config_hash({"b": 2, "a": 1}) right = stable_config_hash({"a": 1, "b": 2}) - assert len(run_id) == 36 + assert len(observability_id) == 36 assert left == right assert left.startswith("sha256:") @@ -67,10 +66,10 @@ def test_contract_models__serialize_expected_shapes(): status="ok", timestamp=timestamp, service="policyengine-simulation-worker", - run_id="run-123", + observability_id="run-123", ) manifest = TracerArtifactManifest( - run_id="run-123", + observability_id="run-123", scenario="baseline", capture_mode=TracerCaptureMode.THRESHOLD, artifact_format="policyengine.flat_trace.v1", @@ -78,7 +77,7 @@ def test_contract_models__serialize_expected_shapes(): generated_at=timestamp, ) response = SimulationCompositeTraceResponse( - run=SimulationRunSummary(run_id="run-123", status="complete"), + run=SimulationRunSummary(observability_id="run-123", status="complete"), timeline=[ SimulationTimelineEntry( stage=SimulationStage.REQUEST_ACCEPTED, @@ -107,15 +106,6 @@ def test_contract_models__serialize_expected_shapes(): assert dumped_version_metrics["versions"] == [] -def test_telemetry_envelope__serializes_expected_defaults(): - envelope = SimulationTelemetryEnvelope(run_id="run-123") - - dumped = envelope.model_dump(mode="json") - - assert dumped["run_id"] == "run-123" - assert dumped["capture_mode"] == "disabled" - - def test_contract_models__reject_extra_fields(): try: SimulationLifecycleEvent( @@ -124,7 +114,7 @@ def test_contract_models__reject_extra_fields(): status="ok", timestamp=datetime(2026, 4, 9, 20, 0, tzinfo=UTC), service="policyengine-simulation-worker", - run_id="run-789", + observability_id="run-789", unexpected=True, ) except ValidationError as error: @@ -133,15 +123,6 @@ def test_contract_models__reject_extra_fields(): raise AssertionError("Expected extra field validation to fail") -def test_contract_models__reject_invalid_enum_values(): - try: - SimulationTelemetryEnvelope(run_id="run-123", capture_mode="bad-mode") - except ValidationError as error: - assert "capture_mode" in str(error) - else: - raise AssertionError("Expected invalid enum validation to fail") - - def test_noop_observability__accepts_calls_without_side_effects(): observability = NoOpObservability() event = SimulationLifecycleEvent( @@ -150,14 +131,14 @@ def test_noop_observability__accepts_calls_without_side_effects(): status="ok", timestamp=datetime(2026, 4, 9, 20, 0, tzinfo=UTC), service="policyengine-simulation-worker", - run_id="run-456", + observability_id="run-456", ) observability.emit_lifecycle_event(event) observability.emit_counter("policyengine.simulation.run.count") observability.emit_histogram("policyengine.simulation.run.duration.seconds", 1.23) with observability.span("run_simulation") as span: - span.set_attribute("run_id", "run-456") + span.set_attribute("observability_id", "run-456") span.add_event("simulation.completed") observability.flush() diff --git a/libs/policyengine-fastapi/uv.lock b/libs/policyengine-fastapi/uv.lock index 27ba2ce22..55bbbd774 100644 --- a/libs/policyengine-fastapi/uv.lock +++ b/libs/policyengine-fastapi/uv.lock @@ -326,18 +326,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -702,18 +690,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -828,15 +804,14 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703, upload-time = "2025-02-04T18:17:13.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955, upload-time = "2025-02-04T18:16:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] @@ -871,7 +846,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -879,14 +854,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5a/4c7f02235ac1269b48f3855f6be1afc641f31d4888d28b90b732fbce7141/opentelemetry_instrumentation-0.51b0.tar.gz", hash = "sha256:4ca266875e02f3988536982467f7ef8c32a38b8895490ddce9ad9604649424fa", size = 27760, upload-time = "2025-02-04T18:21:09.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/2c/48fa93f1acca9f79a06da0df7bfe916632ecc7fce1971067b3e46bcae55b/opentelemetry_instrumentation-0.51b0-py3-none-any.whl", hash = "sha256:c6de8bd26b75ec8b0e54dff59e198946e29de6a10ec65488c357d4b34aa5bdcf", size = 30923, upload-time = "2025-02-04T18:19:37.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -895,14 +870,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/67/8aa6e1129f641f0f3f8786e6c5d18c1f2bbe490bd4b0e91a6879e85154d2/opentelemetry_instrumentation_asgi-0.51b0.tar.gz", hash = "sha256:b3fe97c00f0bfa934371a69674981d76591c68d937b6422a5716ca21081b4148", size = 24201, upload-time = "2025-02-04T18:21:14.321Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/0a95ab37302729543631a789ba8e71dea75c520495739dbbbdfdc580b401/opentelemetry_instrumentation_asgi-0.51b0-py3-none-any.whl", hash = "sha256:e8072993db47303b633c6ec1bc74726ba4d32bd0c46c28dfadf99f79521a324c", size = 16340, upload-time = "2025-02-04T18:19:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -911,27 +886,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/8db4422b5084177d1ef6c7855c69bf2e9e689f595a4a9b59e60588e0d427/opentelemetry_instrumentation_fastapi-0.51b0.tar.gz", hash = "sha256:1624e70f2f4d12ceb792d8a0c331244cd6723190ccee01336273b4559bc13abc", size = 19249, upload-time = "2025-02-04T18:21:28.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117, upload-time = "2025-02-04T18:20:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, ] [[package]] name = "opentelemetry-instrumentation-logging" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/01/922f37e5d8c74bc3f92f24f3e240879ae1d04710fdeebb74f39914fc8467/opentelemetry_instrumentation_logging-0.51b0.tar.gz", hash = "sha256:2adaba90fab85a80de24728153b42ef2a74dd80fda5a3b17f3f95c11bb27c3ff", size = 9754, upload-time = "2025-02-04T18:21:33.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/e3/2d2390370b8730b6b9a2341892cd5508d0f5fa3fe30977457f89979d4085/opentelemetry_instrumentation_logging-0.51b0-py3-none-any.whl", hash = "sha256:b0855cf8b3e37ae4fdb5b77982fbfa713778762c58db3071eb3ca6c97456a049", size = 12171, upload-time = "2025-02-04T18:20:25.006Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -940,9 +916,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/b2/970b1b46576b663bba64503486afe266c064c2bfd1862876420714ce29d9/opentelemetry_instrumentation_sqlalchemy-0.51b0.tar.gz", hash = "sha256:dbfe95b69006017f903dda194606be458d54789e6b3419d37161fb8861bb98a5", size = 14582, upload-time = "2025-02-04T18:21:46.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9d/72af21527ce6f286ac78dc2c75ce44b76cc45343a28f0bcbb13f213421fc/opentelemetry_instrumentation_sqlalchemy-0.65b0.tar.gz", hash = "sha256:8ec2e79f1e00808c5dc639ab5b2cfcdf9dcdef55efd6442c6019707fe2894028", size = 18007, upload-time = "2026-07-16T15:26:19.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/d4/b68c3b3388dd5107f3ed532747e112249c152ba44af71a1f96673d66e3ee/opentelemetry_instrumentation_sqlalchemy-0.51b0-py3-none-any.whl", hash = "sha256:5ff4816228b496aef1511149e2b17a25e0faacec4d5eb65bf18a9964af40f1af", size = 14132, upload-time = "2025-02-04T18:20:50.513Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/3eb3195a60b894cc1852a5657a2b64764a998085c50d7fb3452148af8f18/opentelemetry_instrumentation_sqlalchemy-0.65b0-py3-none-any.whl", hash = "sha256:4d8a2e5afc7b505a48d05cb1fb6db5f8b31681814c1d7767bd6d4b5bdd3a3047", size = 14410, upload-time = "2026-07-16T15:25:32.04Z" }, ] [[package]] @@ -962,38 +938,38 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633, upload-time = "2025-02-04T18:17:28.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717, upload-time = "2025-02-04T18:17:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191, upload-time = "2025-02-04T18:17:29.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416, upload-time = "2025-02-04T18:17:11.305Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/64/32510c0a803465eb6ef1f5bd514d0f5627f8abc9444ed94f7240faf6fcaa/opentelemetry_util_http-0.51b0.tar.gz", hash = "sha256:05edd19ca1cc3be3968b1e502fd94816901a365adbeaab6b6ddb974384d3a0b9", size = 8043, upload-time = "2025-02-04T18:21:59.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/dd/c371eeb9cc78abbdad231a27ce1a196a37ef96328d876ccbb381dea4c8ee/opentelemetry_util_http-0.51b0-py3-none-any.whl", hash = "sha256:0561d7a6e9c422b9ef9ae6e77eafcfcd32a2ab689f5e801475cbb67f189efa20", size = 7304, upload-time = "2025-02-04T18:21:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] [[package]] @@ -1067,10 +1043,10 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.8,<0.116.0" }, { name = "opentelemetry-exporter-gcp-monitoring", specifier = ">=1.9.0a0,<2.0.0" }, { name = "opentelemetry-exporter-gcp-trace", specifier = ">=1.9.0,<2.0.0" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-logging", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.51b0" }, - { name = "opentelemetry-sdk", specifier = ">=1.30.0,<2.0.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-sdk", specifier = ">=1.44.0,<2.0.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1782,12 +1758,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] diff --git a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/budget_window_state.py b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/budget_window_state.py index c451bc3d1..85d3cddf4 100644 --- a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/budget_window_state.py +++ b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/budget_window_state.py @@ -56,6 +56,7 @@ def create_initial_batch_state( resolved_version: str, resolved_app_name: str, bundle: PolicyEngineBundle, + observability_id: str | None = None, ) -> BudgetWindowBatchState: years = _build_years(request.start_year, request.window_size) now = _utc_now_iso() @@ -84,7 +85,7 @@ def create_initial_batch_state( error=None, created_at=now, updated_at=now, - run_id=request.telemetry.run_id if request.telemetry else None, + observability_id=observability_id, ) @@ -257,5 +258,4 @@ def build_batch_status_response( errors=state.errors, resolved_app_name=state.resolved_app_name, policyengine_bundle=state.policyengine_bundle, - run_id=state.run_id, ) diff --git a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/gateway_models.py b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/gateway_models.py index 5762f8e20..cf9d77621 100644 --- a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/gateway_models.py +++ b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/gateway_models.py @@ -34,7 +34,9 @@ MAX_GATEWAY_REQUEST_BYTES = 262_144 -INTERNAL_PASSTHROUGH_FIELDS = frozenset({"_metadata", "_runtime_bundle"}) +INTERNAL_PASSTHROUGH_FIELDS = frozenset( + {"_metadata", "_observability_context", "_runtime_bundle"} +) class PolicyParameterChanges(RootModel[JsonObject]): @@ -183,7 +185,6 @@ class JobSubmitResponse(BaseModel): version: str resolved_app_name: str policyengine_bundle: PolicyEngineBundle - run_id: Optional[str] = None class JobStatusResponse(BaseModel): @@ -195,7 +196,6 @@ class JobStatusResponse(BaseModel): error: Optional[str] = None resolved_app_name: Optional[str] = None policyengine_bundle: Optional[PolicyEngineBundle] = None - run_id: Optional[str] = None class BudgetWindowBatchRequest(GatewayRequestBase): @@ -297,7 +297,6 @@ class BudgetWindowBatchSubmitResponse(BaseModel): version: str resolved_app_name: str policyengine_bundle: PolicyEngineBundle - run_id: Optional[str] = None class BudgetWindowBatchStatusResponse(BaseModel): @@ -315,7 +314,6 @@ class BudgetWindowBatchStatusResponse(BaseModel): error: Optional[str] = None resolved_app_name: Optional[str] = None policyengine_bundle: Optional[PolicyEngineBundle] = None - run_id: Optional[str] = None class BudgetWindowBatchState(BaseModel): @@ -347,7 +345,7 @@ class BudgetWindowBatchState(BaseModel): error: Optional[str] = None created_at: str updated_at: str - run_id: Optional[str] = None + observability_id: Optional[str] = None class PingRequest(BaseModel): diff --git a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/stage12_execution.py b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/stage12_execution.py index 5b8b44c89..d6deecd0e 100644 --- a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/stage12_execution.py +++ b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/stage12_execution.py @@ -425,6 +425,7 @@ class ComparisonReportRecord(StrictContractModel): environment: ContractText calculation_flow: ContractText originating_request_id: ContractText + observability_id: ContractText | None = None production_identity: ContractText incumbent_execution_id: ContractText | None = None worker_version: ContractText diff --git a/libs/policyengine-simulation-contract/tests/test_budget_window_state.py b/libs/policyengine-simulation-contract/tests/test_budget_window_state.py index 6202fc5a7..beccd5c27 100644 --- a/libs/policyengine-simulation-contract/tests/test_budget_window_state.py +++ b/libs/policyengine-simulation-contract/tests/test_budget_window_state.py @@ -21,7 +21,7 @@ ) -def test_create_initial_batch_state_builds_queued_years_and_run_id(): +def test_create_initial_batch_state_builds_queued_years_and_observability_id(): request = BudgetWindowBatchRequest( country="us", region="us", @@ -31,8 +31,7 @@ def test_create_initial_batch_state_builds_queued_years_and_run_id(): scope="macro", reform={}, _telemetry={ - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, ) @@ -43,6 +42,7 @@ def test_create_initial_batch_state_builds_queued_years_and_run_id(): resolved_version="1.500.0", resolved_app_name="policyengine-simulation-py4-10-0", bundle=PolicyEngineBundle(model_version="1.500.0"), + observability_id="00000000-0000-4000-8000-000000000001", ) assert state.batch_job_id == "fc-parent-123" @@ -54,7 +54,7 @@ def test_create_initial_batch_state_builds_queued_years_and_run_id(): assert "data" not in state.request_payload assert state.request_payload["scope"] == "macro" assert state.request_payload["reform"] == {} - assert state.run_id == "batch-run-123" + assert state.observability_id == "00000000-0000-4000-8000-000000000001" def test_build_batch_status_response_computes_progress_from_completed_years(): diff --git a/libs/policyengine-simulation-contract/tests/test_gateway_models.py b/libs/policyengine-simulation-contract/tests/test_gateway_models.py index 23a4d55e1..c1f6dd2e3 100644 --- a/libs/policyengine-simulation-contract/tests/test_gateway_models.py +++ b/libs/policyengine-simulation-contract/tests/test_gateway_models.py @@ -3,9 +3,8 @@ import json import pytest -from pydantic import ValidationError - from policyengine_simulation_contract.gateway_models import ( + MAX_GATEWAY_REQUEST_BYTES, BatchChildJobStatus, BudgetWindowAnnualImpact, BudgetWindowBatchRequest, @@ -16,7 +15,6 @@ HealthResponse, JobStatusResponse, JobSubmitResponse, - MAX_GATEWAY_REQUEST_BYTES, PingRequest, PingResponse, ReadinessResponse, @@ -24,6 +22,7 @@ VersionMap, VersionsResponse, ) +from pydantic import ValidationError class TestPingRequest: @@ -180,6 +179,34 @@ def test_simulation_request_accepts_policyengine_version(self): assert request.version is None assert request.policyengine_version == "4.10.0" + @pytest.mark.parametrize( + "field", + ["observability_id", "run_id", "request_id", "traceparent"], + ) + def test_simulation_request_discards_noncanonical_context_fields(self, field): + request = SimulationRequest( + country="us", + _telemetry={ + field: "identifier", + "submission_claim_id": "claim-123", + "capture_mode": "disabled", + }, + ) + + assert request.telemetry is not None + assert request.telemetry.submission_claim_id == "claim-123" + assert field not in request.telemetry.model_dump() + + def test_simulation_request_maps_legacy_process_id(self): + request = SimulationRequest( + country="us", + _telemetry={"process_id": "legacy-claim-123"}, + ) + + assert request.telemetry is not None + assert request.telemetry.submission_claim_id == "legacy-claim-123" + assert "process_id" not in request.telemetry.model_dump() + def test_simulation_request_accepts_documented_simulation_fields(self): """ Given the documented simulation fields (reform, region, scope) @@ -303,15 +330,13 @@ def test_simulation_request_accepts_typed_telemetry_envelope(self): request = SimulationRequest( country="us", _telemetry={ - "run_id": "run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, ) assert request.telemetry is not None - assert request.telemetry.run_id == "run-123" - assert request.telemetry.process_id == "proc-123" + assert request.telemetry.submission_claim_id == "proc-123" class TestJobSubmitResponse: @@ -551,14 +576,13 @@ def test_budget_window_batch_request_accepts_internal_telemetry_alias(self): start_year="2026", window_size=10, _telemetry={ - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, ) assert request.telemetry is not None - assert request.telemetry.run_id == "batch-run-123" + assert request.telemetry.submission_claim_id == "proc-123" class TestBudgetWindowBatchSubmitResponse: @@ -576,7 +600,6 @@ def test_budget_window_batch_submit_response_serializes_correctly(self): "model_version": "1.500.0", "dataset": "default", }, - run_id="batch-run-123", ) assert response.model_dump(mode="json") == { @@ -593,7 +616,6 @@ def test_budget_window_batch_submit_response_serializes_correctly(self): "spm": None, "dataset": "default", }, - "run_id": "batch-run-123", } diff --git a/libs/policyengine-simulation-contract/uv.lock b/libs/policyengine-simulation-contract/uv.lock index e6a66d1c6..4bba93ea6 100644 --- a/libs/policyengine-simulation-contract/uv.lock +++ b/libs/policyengine-simulation-contract/uv.lock @@ -138,15 +138,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] -[[package]] -name = "asgiref" -version = "3.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -224,6 +215,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -357,12 +421,53 @@ wheels = [ ] [[package]] -name = "executing" -version = "2.2.1" +name = "cryptography" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, ] [[package]] @@ -454,6 +559,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-api-core" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/e5/18aeff14213db86267a0f79d869352954394ab4ab3a3866d9b9b0e82dc6a/google_api_core-2.38.0.tar.gz", hash = "sha256:31e326eafa31b34f1db7a715f50f61dbca7e6e37277762f252ed90e6ce94c246", size = 206784, upload-time = "2026-09-17T20:30:08.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/55/e545e4b0040cb812f65d2abc545c6420f57b687d2af130a447c86d14d00b/google_api_core-2.38.0-py3-none-any.whl", hash = "sha256:8db2730375eba434bd75041fbe06b5374287407689ce132ac5650c8c8f042fcc", size = 187996, upload-time = "2026-09-17T20:29:32.813Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, +] + +[[package]] +name = "google-cloud-appengine-logging" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, +] + +[[package]] +name = "google-cloud-logging" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -466,35 +670,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, +] + [[package]] name = "grpcio" -version = "1.81.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, - { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, - { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -550,18 +797,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] -[[package]] -name = "importlib-metadata" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -571,24 +806,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "logfire" -version = "4.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/f2/34b8ebbd6bbd82c71055d6b881b24d8ada79a0e6692d3dd8cca5e86fadb3/logfire-4.37.0.tar.gz", hash = "sha256:7ee0cb64b59c356a41a1701fb84597037f8db1fa15df7a3715ef363e5a1de06a", size = 1212176, upload-time = "2026-06-12T20:47:06.904Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/08/1805d2f26955671115aae555d78cc4c72a6fe733f332d44d69756bc1737b/logfire-4.37.0-py3-none-any.whl", hash = "sha256:a20823e6dbb3204614a3ea5e79c91df42405c5112393ec9d8e34ef45b60d315f", size = 378930, upload-time = "2026-06-12T20:47:03.674Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -735,31 +952,31 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -770,138 +987,48 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/7e/83986f27b421de04fab1e1a84e892621dac42e6432a9c66779505f4d1381/opentelemetry_instrumentation_asgi-0.63b1-py3-none-any.whl", hash = "sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e", size = 15906, upload-time = "2026-05-21T16:35:04.162Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/d6/0c128fac2e34b7d526a8d3c6edc45b875a97f8a987861b00511151b6337d/opentelemetry_instrumentation_fastapi-0.63b1.tar.gz", hash = "sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba", size = 25387, upload-time = "2026-05-21T16:36:32.071Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/3d/2eae63f13f36d7a8ab5bf03d06ecaf169c2069b524547f24947be6d92094/opentelemetry_instrumentation_fastapi-0.63b1-py3-none-any.whl", hash = "sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281", size = 12795, upload-time = "2026-05-21T16:35:28.68Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/27/c2b4335bca030e893acbe5ff2b4f434868773bf94508be7e6bf5af981b24/opentelemetry_instrumentation_httpx-0.63b1.tar.gz", hash = "sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4", size = 23557, upload-time = "2026-05-21T16:36:34.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/b8/f536780996195c3b9f2354998554671e05a7a262df8c043f63fe9e5a6f0b/opentelemetry_instrumentation_httpx-0.63b1-py3-none-any.whl", hash = "sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86", size = 16336, upload-time = "2026-05-21T16:35:32.221Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.63b1" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/d8/7bf5e4cec0578ac3c28c18eb7b88f34279139cbc8c568d6aa02b9c5ae53e/opentelemetry_util_http-0.63b1.tar.gz", hash = "sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff", size = 11102, upload-time = "2026-05-21T16:36:56.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/f1/34e047e8f6a3c67e5220acf1af7b9f62868c25d77791bca74457bd2180a6/opentelemetry_util_http-0.63b1-py3-none-any.whl", hash = "sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8", size = 8205, upload-time = "2026-05-21T16:36:09.736Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] @@ -942,25 +1069,26 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "1.3.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2a/5e0e4c0f30b20a59ffff14364e15374f9eee494f658f41559aba97c152a2/policyengine_observability-1.3.0.tar.gz", hash = "sha256:4c2f3bb2ee59886aef6c90f1edb9c0b957ace75b1518616c0e61905b8be431e0", size = 107961, upload-time = "2026-07-01T21:10:53.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/a2/80e4bc1c4923e8f4c04483bfd22df2500b0d132c4ce1a6dec6749c94d947/policyengine_observability-1.3.0-py3-none-any.whl", hash = "sha256:7a9444e2cb8ddd7f6d54d601bd1e0a7651bd44783b78627b293a27e741f91930", size = 31087, upload-time = "2026-07-01T21:10:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-contract" @@ -1008,9 +1136,7 @@ version = "0.1.0" source = { editable = "../policyengine-simulation-observability" } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -1019,9 +1145,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1107,6 +1231,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "proto-plus" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -1122,6 +1258,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -1462,59 +1628,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "wrapt" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, - { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, - { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, - { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, - { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, - { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, - { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, -] - [[package]] name = "yarl" version = "1.24.2" @@ -1579,12 +1692,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] - -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -] diff --git a/libs/policyengine-simulation-observability/README.md b/libs/policyengine-simulation-observability/README.md index 7635e90f5..01ee642bd 100644 --- a/libs/policyengine-simulation-observability/README.md +++ b/libs/policyengine-simulation-observability/README.md @@ -1,15 +1,23 @@ # policyengine-simulation-observability -Shared observability plumbing for the simulation entrypoint, gateway, and -executor: the `policyengine-observability` configuration wrapper, legacy -Logfire helpers, error redaction/reporting, and the telemetry envelope. Each -FastAPI service supplies its own service name, while -`X-PolicyEngine-Request-Id` is the single request-correlation header across -service boundaries. - -This lives in its own lib because it has a different reason to change than -the gateway↔executor contract: Logfire is retained only while a replacement -observability platform is evaluated, so the legacy pieces here have a -planned removal date — and this dependency cluster (logfire, -policyengine-observability) is the one that caused issue #602, so it is -quarantined behind one boundary. +Shared API v1 observability configuration for the simulation entry service, +Modal gateway, and Modal executor. Each process creates and owns a +`policyengine-observability` 3.x runtime with an explicit service name, +role, platform, and environment. + +Cloud Run services emit one line of structured JSON to standard output. Modal +services add a bounded Cloud Logging destination when +`OBSERVABILITY_LOGGING_PROJECT_ID` and `OBSERVABILITY_LOG_NAME` are configured. +The log destination is independent from the trace and metric destination, +which uses the standard `OTEL_EXPORTER_OTLP_*` settings. Google authentication +is enabled only when the corresponding workload identity and audience values +are supplied. + +The deployment workflow passes these settings from GitHub environment or +repository variables. This library contains no Google Cloud project, log name, +collector URL, service account, or identity provider default. + +`X-PolicyEngine-Request-Id`, W3C trace headers, and the bounded internal +`_observability_context` object provide correlation across HTTP and Modal job +dispatch. Request bodies, reform definitions, household data, credentials, and +person-level values are excluded from telemetry. diff --git a/libs/policyengine-simulation-observability/pyproject.toml b/libs/policyengine-simulation-observability/pyproject.toml index 0eb549958..b6ce29a06 100644 --- a/libs/policyengine-simulation-observability/pyproject.toml +++ b/libs/policyengine-simulation-observability/pyproject.toml @@ -10,11 +10,7 @@ requires-python = ">=3.13" dependencies = [ "fastapi>=0.115.0", "pydantic>=2.0", - "policyengine-observability[fastapi]>=1.3.0,<2", - "logfire>=3.0.0", - # logfire imports importlib_metadata unconditionally on Python 3.13 - # but does not declare it (see issue #602). - "importlib-metadata>=8", + "policyengine-observability[fastapi,google,otlp-grpc]>=3,<4", ] [project.optional-dependencies] diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/errors.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/errors.py index 90b713a1f..eb52a2750 100644 --- a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/errors.py +++ b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/errors.py @@ -7,11 +7,10 @@ caller a stable generic message plus a correlation id they can cite to support. -The stdlib logger is the guaranteed server-side sink: it always fires, so the -correlation id in the caller-facing message always maps to a log line with the -full message and stack trace. ``policyengine-observability`` and legacy -Logfire (kept while we evaluate replacing it) are best-effort on top — both -silently no-op when their runtime is disabled or unconfigured. +The standard library logger is the guaranteed server-side sink: it always +fires, so the correlation id in the caller-facing message maps to a log line +with the full message and stack trace. The shared observability runtime also +records a bounded structured event on a best-effort basis. Two helpers live here: @@ -27,21 +26,11 @@ import uuid from typing import Any -from policyengine_observability import record_error, record_event -from policyengine_simulation_observability.logfire_legacy import ( - legacy_logfire_attributes, - logfire_is_configured, -) +from policyengine_observability import ObservabilityRuntime logger = logging.getLogger(__name__) -try: - import logfire as _logfire # type: ignore -except Exception: # pragma: no cover - logfire optional locally - _logfire = None - - GENERIC_JOB_FAILURE_MESSAGE = "Simulation failed" @@ -52,6 +41,7 @@ def make_correlation_id() -> str: def log_and_redact_exception( exc: BaseException, *, + runtime: ObservabilityRuntime, scope: str, context: dict[str, Any] | None = None, ) -> str: @@ -69,15 +59,12 @@ def log_and_redact_exception( "correlation_id": correlation_id, "scope": scope, "error_type": type(exc).__name__, - **legacy_logfire_attributes(), } if context: payload.update(context) - # Guaranteed sink. record_error/record_event silently no-op when the - # observability runtime is disabled or unconfigured (they never raise), - # so this stdlib line is what makes the correlation id in the caller's - # message always resolvable to a full message + stack server-side. + # This local record keeps the correlation id resolvable even if every + # remote exporter is unavailable. try: logger.error( "Gateway %s failed (correlation_id=%s)", @@ -95,21 +82,13 @@ def log_and_redact_exception( ) try: - record_error(exc, handled=True, status_code=500) - record_event( + if isinstance(exc, Exception): + runtime.record_exception(exc, handled=True, status_code=500) + runtime.event( "gateway_error_redacted", - **payload, + attributes=payload, ) except Exception: # pragma: no cover - defensive, never raise from logger pass - if logfire_is_configured(): - try: - _logfire.exception( # type: ignore[union-attr] - "Gateway {scope} failed", - **payload, - ) - except Exception: # pragma: no cover - defensive, never raise from logger - pass - return f"{GENERIC_JOB_FAILURE_MESSAGE} (correlation_id={correlation_id})" diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/identifiers.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/identifiers.py new file mode 100644 index 000000000..dfe54d716 --- /dev/null +++ b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/identifiers.py @@ -0,0 +1,35 @@ +"""Diagnostic correlation identifiers shared by simulation services.""" + +from __future__ import annotations + +from typing import Any +from uuid import UUID, uuid4 + +OBSERVABILITY_ID_HEADER = "X-PolicyEngine-Observability-Id" + + +def generate_observability_id() -> str: + """Create a diagnostic identifier with no application semantics.""" + + return str(uuid4()) + + +def resolve_observability_id(value: Any) -> str: + """Use a valid caller value or create a new diagnostic identifier.""" + + return normalize_observability_id(value) or generate_observability_id() + + +def normalize_observability_id(value: Any) -> str | None: + """Return a canonical UUID string or ``None`` for malformed input. + + Observability metadata is best effort. Callers must never reject or alter a + simulation because this value is absent or malformed. + """ + + if not isinstance(value, str): + return None + try: + return str(UUID(value)) + except (ValueError, AttributeError): + return None diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/logfire_legacy.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/logfire_legacy.py deleted file mode 100644 index f92c670b3..000000000 --- a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/logfire_legacy.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Legacy Logfire helpers used while evaluating a replacement platform.""" - -from __future__ import annotations - -import os -from contextlib import nullcontext -from typing import Any - -from policyengine_simulation_observability.observability import ( - logfire_replacement_attributes, -) - - -# Whether configure_logfire successfully configured export in this process. -# Logfire's own ``config.send_to_logfire`` flag defaults to ``True`` on an -# UNCONFIGURED instance, so it cannot be used to answer "did we configure -# Logfire?" — callers must consult this flag instead. -_logfire_configured = False - - -def legacy_logfire_attributes() -> dict[str, str]: - return logfire_replacement_attributes() - - -def logfire_is_configured() -> bool: - """Whether configure_logfire ran with a token in this process.""" - return _logfire_configured - - -def configure_logfire( - service_name: str, - *, - default_environment: str | None = None, -) -> bool: - """Configure legacy Logfire export if the Modal secret is present.""" - global _logfire_configured - - token = os.environ.get("LOGFIRE_TOKEN", "") - if not token: - _logfire_configured = False - return False - - import logfire - - logfire.configure( - service_name=service_name, - token=token, - environment=( - os.environ.get("LOGFIRE_ENVIRONMENT") - or default_environment - or os.environ.get("MODAL_ENVIRONMENT") - or "production" - ), - console=False, - ) - _logfire_configured = True - return True - - -def logfire_span(enabled: bool, name: str, **attrs: Any): - if not enabled: - return nullcontext() - - import logfire - - return logfire.span(name, **attrs) - - -def flush_logfire(enabled: bool) -> None: - if not enabled: - return - - import logfire - - logfire.force_flush() diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/observability.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/observability.py index aac5399f4..9d098b903 100644 --- a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/observability.py +++ b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/observability.py @@ -1,255 +1,262 @@ from __future__ import annotations import os +from collections.abc import Awaitable, Callable from dataclasses import replace -from enum import StrEnum -from typing import Any +from importlib.metadata import PackageNotFoundError, version +from typing import Any, Literal from fastapi import FastAPI from policyengine_observability import ( - UNKNOWN_SEGMENT, + DeploymentIdentity, + GoogleCloudLogDestination, + GoogleCloudLogFormatter, + LogDestinationStrategy, + LoggingConfig, ObservabilityConfig, ObservabilityRuntime, - set_observability_runtime, -) -from policyengine_observability.adapters.fastapi import ( - init_fastapi_observability, + ServiceIdentity, + StdoutLogDestination, + configure, + instrument_fastapi, ) +from starlette.datastructures import Headers +from policyengine_simulation_observability.identifiers import ( + OBSERVABILITY_ID_HEADER, + normalize_observability_id, +) -SERVICE_NAME = "policyengine-simulation-executor" -SPAN_PREFIX = "simulation" -LOG_DESTINATIONS = ("stdout",) -LOGFIRE_STATUS = "legacy_candidate_for_replacement" -LOGFIRE_REPLACEMENT_CANDIDATE = "policyengine-observability" -SIMULATION_METRIC_ATTRIBUTE_KEYS = ( - "batch_job_id", - "country", - "function_call_id", - "geography_code", - "geography_type", - "job_id", - "modal_app_name", - "modal_environment", - "modal_function_name", - "platform", - "policyengine_version", - "process_id", - "region", - "request_id", - "resolved_app_name", - "resolved_version", - "run_id", - "logfire_status", - "logfire_replacement_candidate", - "runtime_role", - "scope", - "simulation_year", +Platform = Literal["google_cloud_run", "modal", "local", "other"] + +MODAL_IMAGE_ENV_NAMES = ( + "OBSERVABILITY_SERVICE_NAMESPACE", + "OBSERVABILITY_TRACE_PROJECT_ID", + "OBSERVABILITY_LOGGING_PROJECT_ID", + "OBSERVABILITY_LOG_NAME", + "OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER", + "OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "POLICYENGINE_OTEL_GOOGLE_AUDIENCE", ) +SIMULATION_ATTRIBUTE_KEYS = frozenset( + { + "ack_value_present", + "backend", + "backoff_sleep_count", + "backoff_sleep_ms_total", + "baseline_artifact", + "batch_job_id", + "capture_mode", + "child_poll_count", + "child_poll_ms_total", + "config_hash", + "correlation_id", + "country", + "data_version", + "dataset", + "elapsed_ms", + "error_type", + "evaluation_id", + "execution_mode", + "function_call_id", + "geography_code", + "geography_type", + "include_cliffs", + "job_id", + "job_state", + "method", + "modal_app_name", + "modal_application", + "modal_environment", + "modal_function_name", + "model_version", + "national_execution", + "parent_call_not_found", + "partition_uncovered_regions", + "policyengine_version", + "submission_claim_id", + "production_identity", + "reason", + "region", + "region_group", + "requested_version", + "request_id", + "runner_name", + "resolved_app_name", + "resolved_version", + "route", + "observability_id", + "coordinator_invocation_id", + "scope", + "segmented", + "segmented_group_count", + "segmented_poll_count", + "simulation_kind", + "simulation_role", + "simulation_year", + "status_code", + "time_period", + "version", + "worker_version", + } +) -class SegmentName(StrEnum): - UNKNOWN = UNKNOWN_SEGMENT - - REQUEST_PARSE = "request_parse" - ROUTE_RESOLUTION = "route_resolution" - POLICYENGINE_BUNDLE = "policyengine_bundle" - # NOTE: there is deliberately no modal_function_lookup segment — - # ``Function.from_name`` is a lazy handle whose control-plane RPC fires - # inside ``spawn``, so lookup + spawn are timed together under - # MODAL_FUNCTION_SPAWN. - MODAL_FUNCTION_SPAWN = "modal_function_spawn" - MODAL_DICT_READ = "modal_dict_read" - MODAL_JOB_METADATA_WRITE = "modal_job_metadata_write" - MODAL_JOB_STATUS_POLL = "modal_job_status_poll" - - CREDENTIAL_SETUP = "credential_setup" - COUNTRY_MODULE_LOAD = "country_module_load" - REGION_RESOLUTION = "region_resolution" - DATASET_RESOLUTION = "dataset_resolution" - DATASET_LOAD = "dataset_load" - POLICY_NORMALIZATION = "policy_normalization" - SIMULATION_BUILD = "simulation_build" - CALCULATION = "calculation" - RESPONSE_SERIALIZATION = "response_serialization" - - BUDGET_WINDOW_CONTEXT = "budget_window_context" - BUDGET_WINDOW_STATE_LOAD = "budget_window_state_load" - BUDGET_WINDOW_STATE_WRITE = "budget_window_state_write" - BUDGET_WINDOW_CHILD_SPAWN = "budget_window_child_spawn" - BUDGET_WINDOW_RESULT_PARSE = "budget_window_result_parse" - BUDGET_WINDOW_AGGREGATION = "budget_window_aggregation" - BUDGET_WINDOW_CHILD_REQUEST_BUILD = "budget_window_child_request_build" - BUDGET_WINDOW_STATUS_SERIALIZATION = "budget_window_status_serialization" - # NOTE: per-iteration poll/sleep segments were removed deliberately — - # the scheduler's poll loop publishes bounded aggregate attributes - # (child_poll_count/child_poll_ms_total, backoff_sleep_count/ - # backoff_sleep_ms_total) instead, because one segment per probe grows - # the operation's segment tree without bound over a long batch. - - SEGMENTED_NATIONAL_CHILD_SPAWN = "segmented_national_child_spawn" - SEGMENTED_NATIONAL_REDUCE = "segmented_national_reduce" - - MODAL_JOB_METADATA_READ = "modal_job_metadata_read" - - SIMULATION_OUTPUT_BUILD = "simulation_output_build" - SIMULATION_OUTPUT_MODEL_DUMP = "simulation_output_model_dump" - ECONOMIC_IMPACT_ANALYSIS = "economic_impact_analysis" - OUTPUT_BUDGETARY_IMPACT = "output_budgetary_impact" - OUTPUT_DETAILED_BUDGET = "output_detailed_budget" - OUTPUT_DECILE = "output_decile" - OUTPUT_INEQUALITY = "output_inequality" - OUTPUT_POVERTY = "output_poverty" - OUTPUT_INTRA_DECILE = "output_intra_decile" - OUTPUT_WEALTH_DECILE = "output_wealth_decile" - OUTPUT_INTRA_WEALTH_DECILE = "output_intra_wealth_decile" - OUTPUT_LABOR_SUPPLY = "output_labor_supply" - OUTPUT_CONGRESSIONAL_DISTRICT = "output_congressional_district" - OUTPUT_UK_CONSTITUENCY = "output_uk_constituency" - OUTPUT_UK_LOCAL_AUTHORITY = "output_uk_local_authority" - OUTPUT_CLIFF = "output_cliff" - OUTPUT_MODEL_VERSION = "output_model_version" - OUTPUT_DATA_VERSION = "output_data_version" - - -def configure_process_observability( - *, - platform: str, - service_role: str, - runtime_role: str | None = None, - modal_app_name: str | None = None, - modal_function_name: str | None = None, -) -> None: - os.environ["OBSERVABILITY_PLATFORM"] = platform - os.environ["OBSERVABILITY_SERVICE_ROLE"] = service_role - os.environ["OBSERVABILITY_RUNTIME_ROLE"] = runtime_role or service_role - if modal_app_name: - os.environ["OBSERVABILITY_MODAL_APP_NAME"] = modal_app_name - if modal_function_name: - os.environ["OBSERVABILITY_MODAL_FUNCTION_NAME"] = modal_function_name +DISPATCH_ATTRIBUTE_KEYS = frozenset({"job_id", "observability_id", "simulation_id"}) + + +class _RuntimeObservabilityIdMiddleware: + """Attach the transport identifier after request tracing has started.""" + + def __init__(self, app: Any, *, runtime: ObservabilityRuntime) -> None: + self.app = app + self.runtime = runtime + + async def __call__( + self, + scope: dict[str, Any], + receive: Callable[[], Awaitable[dict[str, Any]]], + send: Callable[[dict[str, Any]], Awaitable[None]], + ) -> None: + if scope.get("type") == "http": + observability_id = normalize_observability_id( + Headers(scope=scope).get(OBSERVABILITY_ID_HEADER) + ) + if observability_id is not None: + try: + self.runtime.set_context(observability_id=observability_id) + except Exception: # noqa: BLE001, S110 - telemetry is non-fatal + pass + await self.app(scope, receive, send) + + +def modal_image_environment() -> dict[str, str]: + """Return deployment-provided settings to bake into a Modal image.""" + + configured = { + name: value + for name in MODAL_IMAGE_ENV_NAMES + if (value := os.getenv(name, "").strip()) + } + return { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_TRACES_EXPORTER": "otlp", + "OTEL_METRICS_EXPORTER": "otlp", + "OTEL_TRACES_SAMPLER_ARG": "1.0", + **configured, + } -def init_simulation_observability( - app: FastAPI, +def build_runtime( *, service_name: str, - service_role: str = "api", + service_role: str, + platform: Platform, + environment: str, + service_version: str | None = None, ) -> ObservabilityRuntime: - service_role = _service_role(service_role) - platform = _platform() - config = _config( - service_name=service_name, - service_role=service_role, - platform=platform, + """Create one explicitly owned simulation observability runtime.""" + + trace_project = os.getenv("OBSERVABILITY_TRACE_PROJECT_ID", "").strip() + logging_project = os.getenv("OBSERVABILITY_LOGGING_PROJECT_ID", "").strip() + log_name = os.getenv("OBSERVABILITY_LOG_NAME", "").strip() + destinations: list[LogDestinationStrategy] = [ + StdoutLogDestination( + formatter=( + GoogleCloudLogFormatter(trace_project) if trace_project else None + ) + ) + ] + if logging_project or log_name: + destinations.append( + GoogleCloudLogDestination( + project_id=logging_project, + log_name=log_name, + ) + ) + config = ObservabilityConfig.from_env( + service=ServiceIdentity( + name=service_name, + namespace=os.getenv( + "OBSERVABILITY_SERVICE_NAMESPACE", + "policyengine.api-v1", + ), + version=service_version or _service_version(), + role=service_role, + ), + deployment=DeploymentIdentity( + environment=environment, + platform=platform, + region=os.getenv("GOOGLE_CLOUD_REGION") or os.getenv("CLOUD_RUN_REGION"), + instance_id=os.getenv("K_REVISION") or os.getenv("MODAL_TASK_ID"), + ), + logging=LoggingConfig( + destinations=tuple(destinations), + capture_standard_library=True, + ), + application_attribute_keys=SIMULATION_ATTRIBUTE_KEYS, + dispatch_attribute_keys=DISPATCH_ATTRIBUTE_KEYS, ) - return init_fastapi_observability( - app, - config=config, - runtime=ObservabilityRuntime(config, segment_registry=SegmentName), - service_name=service_name, - service_role=service_role, - span_prefix=SPAN_PREFIX, - segment_registry=SegmentName, - static_attributes=_metadata(service_role, platform), + config = replace( + config, + otel=replace(config.otel, sampling_ratio=1.0), ) + return configure(config) -def init_process_observability( +def init_simulation_observability( + app: FastAPI, *, + service_name: str, service_role: str, + platform: Platform, + environment: str, + service_version: str | None = None, ) -> ObservabilityRuntime: - service_role = _service_role(service_role) - platform = _platform() - config = _config( - service_name=SERVICE_NAME, + runtime = build_runtime( + service_name=service_name, service_role=service_role, platform=platform, + environment=environment, + service_version=service_version, ) - runtime = ObservabilityRuntime(config, segment_registry=SegmentName) - runtime.configure() - set_observability_runtime(runtime) - return runtime + # FastAPI applies the most recently registered middleware first. Install + # this context layer before the lifecycle integration so it executes after + # begin_request has created the request-local runtime state. Application + # correlation middleware may then be registered outside both layers and + # normalize the header before either observability layer reads it. + app.add_middleware(_RuntimeObservabilityIdMiddleware, runtime=runtime) + return instrument_fastapi(app, runtime) -def process_static_attributes(*, service_role: str) -> dict[str, Any]: - """Static identity attributes for non-FastAPI (worker) operations. - - The FastAPI adapter applies these per request via ``static_attributes``; - plain-process runtimes have no equivalent hook, so worker entrypoints must - merge this dict into their ``operation(...)`` attributes explicitly — - otherwise operation logs carry no platform/Modal identity. - """ - return _metadata(_service_role(service_role), _platform()) - - -def logfire_replacement_attributes() -> dict[str, str]: - return { - "logfire_status": LOGFIRE_STATUS, - "logfire_replacement_candidate": LOGFIRE_REPLACEMENT_CANDIDATE, - } - - -def _config( +def init_process_observability( *, service_name: str, service_role: str, - platform: str, -) -> ObservabilityConfig: - config = ObservabilityConfig.from_env( + platform: Platform, + environment: str, + service_version: str | None = None, +) -> ObservabilityRuntime: + return build_runtime( service_name=service_name, service_role=service_role, - span_prefix=SPAN_PREFIX, - extra_metric_attribute_keys=SIMULATION_METRIC_ATTRIBUTE_KEYS, - default_log_destinations=LOG_DESTINATIONS, - ) - return replace( - config, - environment=_environment(), - log_destinations=LOG_DESTINATIONS, - otel_enabled=False, - google_cloud_project=None, - ) - - -def _environment() -> str: - return ( - os.getenv("OBSERVABILITY_ENVIRONMENT") - or os.getenv("MODAL_ENVIRONMENT") - or os.getenv("DEPLOYMENT_ENVIRONMENT") - or os.getenv("APP_ENVIRONMENT") - or os.getenv("APP_ENV") - or os.getenv("ENVIRONMENT") - or "local" - ) - - -def _service_role(default: str) -> str: - return ( - os.getenv("OBSERVABILITY_SERVICE_ROLE") - or os.getenv("OBSERVABILITY_RUNTIME_ROLE") - or default + platform=platform, + environment=environment, + service_version=service_version, ) -def _platform() -> str: - configured = os.getenv("OBSERVABILITY_PLATFORM") - if configured: - return configured - if os.getenv("MODAL_ENVIRONMENT") or os.getenv("MODAL_TASK_ID"): - return "modal" - return "local" - - -def _metadata(service_role: str, platform: str) -> dict[str, Any]: - values = { - "platform": platform, - "runtime_role": os.getenv("OBSERVABILITY_RUNTIME_ROLE") or service_role, - "modal_environment": os.getenv("MODAL_ENVIRONMENT"), - "modal_app_name": os.getenv("OBSERVABILITY_MODAL_APP_NAME"), - "modal_function_name": os.getenv("OBSERVABILITY_MODAL_FUNCTION_NAME"), - **logfire_replacement_attributes(), - } - return {key: value for key, value in values.items() if value} - - -set_observability_runtime(ObservabilityRuntime.disabled()) +def _service_version() -> str: + for name in ( + "POLICYENGINE_SERVICE_VERSION", + "K_REVISION", + "POLICYENGINE_VERSION", + ): + value = os.getenv(name) + if value: + return value + try: + return version("policyengine-simulation-observability") + except PackageNotFoundError: + return "0.1.0" diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/stages.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/stages.py new file mode 100644 index 000000000..18982c592 --- /dev/null +++ b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/stages.py @@ -0,0 +1,240 @@ +"""Canonical stage registry for every simulation execution configuration. + +Runtime code imports the named registry entry for the configuration it is +executing. This keeps trace span names and their expected order in one place +instead of distributing string literals across the gateway and workers. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from types import MappingProxyType +from typing import Mapping + + +class RunConfiguration(StrEnum): + ANNUAL_IMPACT = "annual_impact" + BUDGET_WINDOW = "budget_window" + SEGMENTED_NATIONAL = "segmented_national" + STAGE12_SHADOW_REPORT = "stage12_shadow_report" + STAGE12_CANONICAL_REPORT = "stage12_canonical_report" + STAGE12_SIMULATION = "stage12_simulation" + + +class Stage(StrEnum): + ANNUAL_EXECUTION = "run_simulation" + BUDGET_WINDOW_EXECUTION = "run_budget_window_batch" + SEGMENTED_NATIONAL_EXECUTION = "run_simulation_segment" + + REQUEST_PARSE = "request_parse" + ROUTE_RESOLUTION = "route_resolution" + POLICYENGINE_BUNDLE = "policyengine_bundle" + MODAL_FUNCTION_SPAWN = "modal_function_spawn" + MODAL_DICT_READ = "modal_dict_read" + MODAL_JOB_METADATA_WRITE = "modal_job_metadata_write" + MODAL_JOB_STATUS_POLL = "modal_job_status_poll" + + CREDENTIAL_SETUP = "credential_setup" + COUNTRY_MODULE_LOAD = "country_module_load" + REGION_RESOLUTION = "region_resolution" + DATASET_RESOLUTION = "dataset_resolution" + DATASET_LOAD = "dataset_load" + POLICY_NORMALIZATION = "policy_normalization" + SIMULATION_BUILD = "simulation_build" + CALCULATION = "calculation" + RESPONSE_SERIALIZATION = "response_serialization" + + BUDGET_WINDOW_CONTEXT = "budget_window_context" + BUDGET_WINDOW_STATE_LOAD = "budget_window_state_load" + BUDGET_WINDOW_STATE_WRITE = "budget_window_state_write" + BUDGET_WINDOW_CHILD_SPAWN = "budget_window_child_spawn" + BUDGET_WINDOW_RESULT_PARSE = "budget_window_result_parse" + BUDGET_WINDOW_AGGREGATION = "budget_window_aggregation" + BUDGET_WINDOW_CHILD_REQUEST_BUILD = "budget_window_child_request_build" + BUDGET_WINDOW_STATUS_SERIALIZATION = "budget_window_status_serialization" + + SEGMENTED_NATIONAL_CHILD_SPAWN = "segmented_national_child_spawn" + SEGMENTED_NATIONAL_REDUCE = "segmented_national_reduce" + MODAL_JOB_METADATA_READ = "modal_job_metadata_read" + + SIMULATION_OUTPUT_BUILD = "simulation_output_build" + SIMULATION_OUTPUT_MODEL_DUMP = "simulation_output_model_dump" + ECONOMIC_IMPACT_ANALYSIS = "economic_impact_analysis" + OUTPUT_BUDGETARY_IMPACT = "output_budgetary_impact" + OUTPUT_DETAILED_BUDGET = "output_detailed_budget" + OUTPUT_DECILE = "output_decile" + OUTPUT_INEQUALITY = "output_inequality" + OUTPUT_POVERTY = "output_poverty" + OUTPUT_INTRA_DECILE = "output_intra_decile" + OUTPUT_WEALTH_DECILE = "output_wealth_decile" + OUTPUT_INTRA_WEALTH_DECILE = "output_intra_wealth_decile" + OUTPUT_LABOR_SUPPLY = "output_labor_supply" + OUTPUT_CONGRESSIONAL_DISTRICT = "output_congressional_district" + OUTPUT_UK_CONSTITUENCY = "output_uk_constituency" + OUTPUT_UK_LOCAL_AUTHORITY = "output_uk_local_authority" + OUTPUT_CLIFF = "output_cliff" + OUTPUT_MODEL_VERSION = "output_model_version" + OUTPUT_DATA_VERSION = "output_data_version" + + STAGE12_ENTRY_DISPATCH = "stage12_entry_dispatch" + STAGE12_COORDINATOR_EXECUTION = "stage12_coordinator_execution" + STAGE12_COORDINATOR_CLAIM = "stage12_coordinator_claim" + STAGE12_CHILD_STATE_CREATE = "stage12_child_state_create" + STAGE12_CHILD_DISPATCH = "stage12_child_dispatch" + STAGE12_CHILD_WAIT = "stage12_child_wait" + STAGE12_INPUT_WRITE = "stage12_input_write" + STAGE12_SIMULATION_EXECUTION = "stage12_simulation_execution" + STAGE12_CALCULATION = "stage12_calculation" + STAGE12_SIMULATION_ARTIFACT_WRITE = "stage12_simulation_artifact_write" + STAGE12_ARTIFACT_READ = "stage12_artifact_read" + STAGE12_AGGREGATION = "stage12_aggregation" + STAGE12_AGGREGATE_ARTIFACT_WRITE = "stage12_aggregate_artifact_write" + STAGE12_RESULT_COMPARISON = "stage12_result_comparison" + + +@dataclass(frozen=True) +class StagePlan: + configuration: RunConfiguration + stages: tuple[Stage, ...] + + def name(self, stage: Stage) -> str: + """Return a configured stage name, rejecting unregistered use.""" + + if stage not in self.stages: + raise ValueError( + f"{stage.value!r} is not registered for {self.configuration.value!r}" + ) + return stage.value + + +_COMMON_SIMULATION = ( + Stage.CREDENTIAL_SETUP, + Stage.COUNTRY_MODULE_LOAD, + Stage.REGION_RESOLUTION, + Stage.DATASET_RESOLUTION, + Stage.DATASET_LOAD, + Stage.POLICY_NORMALIZATION, + Stage.SIMULATION_BUILD, + Stage.CALCULATION, + Stage.SIMULATION_OUTPUT_BUILD, + Stage.ECONOMIC_IMPACT_ANALYSIS, + Stage.OUTPUT_MODEL_VERSION, + Stage.OUTPUT_DATA_VERSION, + Stage.OUTPUT_BUDGETARY_IMPACT, + Stage.OUTPUT_DETAILED_BUDGET, + Stage.OUTPUT_DECILE, + Stage.OUTPUT_INEQUALITY, + Stage.OUTPUT_POVERTY, + Stage.OUTPUT_INTRA_DECILE, + Stage.OUTPUT_WEALTH_DECILE, + Stage.OUTPUT_INTRA_WEALTH_DECILE, + Stage.OUTPUT_LABOR_SUPPLY, + Stage.OUTPUT_CONGRESSIONAL_DISTRICT, + Stage.OUTPUT_UK_CONSTITUENCY, + Stage.OUTPUT_UK_LOCAL_AUTHORITY, + Stage.OUTPUT_CLIFF, + Stage.RESPONSE_SERIALIZATION, + Stage.SIMULATION_OUTPUT_MODEL_DUMP, +) + +_ANNUAL_IMPACT = ( + Stage.ANNUAL_EXECUTION, + Stage.ROUTE_RESOLUTION, + Stage.REQUEST_PARSE, + Stage.POLICYENGINE_BUNDLE, + Stage.MODAL_FUNCTION_SPAWN, + Stage.MODAL_JOB_METADATA_WRITE, + Stage.MODAL_JOB_METADATA_READ, + Stage.MODAL_JOB_STATUS_POLL, + Stage.MODAL_DICT_READ, + *_COMMON_SIMULATION, +) + +_BUDGET_WINDOW = ( + Stage.BUDGET_WINDOW_EXECUTION, + Stage.ROUTE_RESOLUTION, + Stage.POLICYENGINE_BUNDLE, + Stage.REQUEST_PARSE, + Stage.MODAL_FUNCTION_SPAWN, + Stage.MODAL_JOB_METADATA_WRITE, + Stage.BUDGET_WINDOW_CONTEXT, + Stage.BUDGET_WINDOW_STATE_LOAD, + Stage.BUDGET_WINDOW_STATE_WRITE, + Stage.BUDGET_WINDOW_CHILD_REQUEST_BUILD, + Stage.BUDGET_WINDOW_CHILD_SPAWN, + Stage.BUDGET_WINDOW_RESULT_PARSE, + Stage.BUDGET_WINDOW_AGGREGATION, + Stage.BUDGET_WINDOW_STATUS_SERIALIZATION, + Stage.MODAL_JOB_STATUS_POLL, + *_COMMON_SIMULATION, +) + +_SEGMENTED_NATIONAL = ( + Stage.SEGMENTED_NATIONAL_EXECUTION, + Stage.REQUEST_PARSE, + Stage.SEGMENTED_NATIONAL_CHILD_SPAWN, + Stage.MODAL_JOB_STATUS_POLL, + Stage.SEGMENTED_NATIONAL_REDUCE, + *_COMMON_SIMULATION, +) + +_STAGE12_COMMON = ( + Stage.STAGE12_ENTRY_DISPATCH, + Stage.STAGE12_COORDINATOR_EXECUTION, + Stage.STAGE12_COORDINATOR_CLAIM, + Stage.STAGE12_CHILD_STATE_CREATE, + Stage.STAGE12_CHILD_DISPATCH, + Stage.STAGE12_CHILD_WAIT, + Stage.STAGE12_ARTIFACT_READ, + Stage.STAGE12_AGGREGATION, + Stage.STAGE12_AGGREGATE_ARTIFACT_WRITE, +) + +RUN_STAGE_REGISTRY: Mapping[RunConfiguration, StagePlan] = MappingProxyType( + { + RunConfiguration.ANNUAL_IMPACT: StagePlan( + RunConfiguration.ANNUAL_IMPACT, _ANNUAL_IMPACT + ), + RunConfiguration.BUDGET_WINDOW: StagePlan( + RunConfiguration.BUDGET_WINDOW, _BUDGET_WINDOW + ), + RunConfiguration.SEGMENTED_NATIONAL: StagePlan( + RunConfiguration.SEGMENTED_NATIONAL, _SEGMENTED_NATIONAL + ), + RunConfiguration.STAGE12_SHADOW_REPORT: StagePlan( + RunConfiguration.STAGE12_SHADOW_REPORT, + (*_STAGE12_COMMON, Stage.STAGE12_RESULT_COMPARISON), + ), + RunConfiguration.STAGE12_CANONICAL_REPORT: StagePlan( + RunConfiguration.STAGE12_CANONICAL_REPORT, _STAGE12_COMMON + ), + RunConfiguration.STAGE12_SIMULATION: StagePlan( + RunConfiguration.STAGE12_SIMULATION, + ( + Stage.STAGE12_SIMULATION_EXECUTION, + Stage.STAGE12_INPUT_WRITE, + Stage.CREDENTIAL_SETUP, + Stage.COUNTRY_MODULE_LOAD, + Stage.REGION_RESOLUTION, + Stage.DATASET_RESOLUTION, + Stage.DATASET_LOAD, + Stage.POLICY_NORMALIZATION, + Stage.SIMULATION_BUILD, + Stage.STAGE12_CALCULATION, + Stage.STAGE12_SIMULATION_ARTIFACT_WRITE, + ), + ), + } +) + +ANNUAL_IMPACT_STAGES = RUN_STAGE_REGISTRY[RunConfiguration.ANNUAL_IMPACT] +BUDGET_WINDOW_STAGES = RUN_STAGE_REGISTRY[RunConfiguration.BUDGET_WINDOW] +SEGMENTED_NATIONAL_STAGES = RUN_STAGE_REGISTRY[RunConfiguration.SEGMENTED_NATIONAL] +STAGE12_SHADOW_REPORT_STAGES = RUN_STAGE_REGISTRY[ + RunConfiguration.STAGE12_SHADOW_REPORT +] +STAGE12_CANONICAL_REPORT_STAGES = RUN_STAGE_REGISTRY[ + RunConfiguration.STAGE12_CANONICAL_REPORT +] +STAGE12_SIMULATION_STAGES = RUN_STAGE_REGISTRY[RunConfiguration.STAGE12_SIMULATION] diff --git a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/telemetry.py b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/telemetry.py index dccb418eb..3c51618eb 100644 --- a/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/telemetry.py +++ b/libs/policyengine-simulation-observability/src/policyengine_simulation_observability/telemetry.py @@ -1,25 +1,54 @@ -""" -Internal telemetry helpers for Modal request passthrough. -""" +"""Allowlisted correlation metadata for asynchronous simulation dispatch.""" from __future__ import annotations from datetime import datetime from typing import Any, Literal -from pydantic import BaseModel, ConfigDict +from pydantic import ( + BaseModel, + ConfigDict, + ValidationError, + field_validator, + model_validator, +) +from policyengine_simulation_observability.identifiers import ( + normalize_observability_id, +) CaptureMode = Literal["disabled", "failures", "threshold", "sampled", "always"] +NONCANONICAL_CONTEXT_FIELDS = frozenset( + {"observability_id", "run_id", "request_id", "traceparent"} +) + + +class ObservabilityContext(BaseModel): + traceparent: str | None = None + tracestate: str | None = None + captured_at: datetime + request_id: str | None = None + job_id: str | None = None + observability_id: str | None = None + simulation_id: str | None = None + + model_config = ConfigDict(extra="forbid") + + @field_validator("observability_id") + @classmethod + def validate_observability_id(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = normalize_observability_id(value) + if normalized is None: + raise ValueError("observability_id must be a UUID") + return normalized class TelemetryEnvelope(BaseModel): - """Minimal shared telemetry payload shape for gateway and worker code.""" + """Bounded internal metadata passed from API request to Modal worker.""" - run_id: str - process_id: str | None = None - request_id: str | None = None - traceparent: str | None = None + submission_claim_id: str | None = None requested_at: datetime | None = None simulation_kind: str | None = None geography_code: str | None = None @@ -29,19 +58,74 @@ class TelemetryEnvelope(BaseModel): model_config = ConfigDict(extra="forbid") + @model_validator(mode="before") + @classmethod + def discard_noncanonical_context_fields(cls, value: Any) -> Any: + """Ignore old body fields without using them as correlation inputs. + + This keeps the receiver compatible with the deployed API during the + rollout. The HTTP header remains the only source of observability_id. + """ + + if not isinstance(value, dict): + return value + normalized = dict(value) + # LEGACY COMPATIBILITY: API v1 sent submission_claim_id as process_id. + # Remove this mapping after the API v1 deployment and rollback period. + legacy_process_id = normalized.pop("process_id", None) + if "submission_claim_id" not in normalized and legacy_process_id is not None: + normalized["submission_claim_id"] = legacy_process_id + return { + key: item + for key, item in normalized.items() + if key not in NONCANONICAL_CONTEXT_FIELDS + } + + +def remote_context(params: dict[str, Any]) -> dict[str, str] | None: + """Return only the correlation fields accepted by the shared runtime.""" + + context = params.get("_observability_context") + if not isinstance(context, dict): + return None + try: + validated = ObservabilityContext.model_validate(context) + except ValidationError: + return None + return { + key: str(value) + for key, value in validated.model_dump(exclude_none=True).items() + } + + +def apply_remote_context( + runtime: Any, + params: dict[str, Any], +) -> dict[str, str] | None: + """Validate remote context and apply its diagnostic identifier locally.""" + + propagated = remote_context(params) + if propagated is not None: + observability_id = propagated.get("observability_id") + if observability_id is not None: + runtime.set_context(observability_id=observability_id) + return propagated + def split_internal_payload( params: dict[str, Any], ) -> tuple[dict[str, Any], TelemetryEnvelope | None, dict[str, Any] | None]: - """Strip internal passthrough fields before SimulationOptions validation.""" - simulation_params = dict(params) raw_telemetry = simulation_params.pop("_telemetry", None) + simulation_params.pop("_observability_context", None) raw_metadata = simulation_params.pop("_metadata", None) telemetry = None if raw_telemetry is not None: - telemetry = TelemetryEnvelope.model_validate(raw_telemetry) + try: + telemetry = TelemetryEnvelope.model_validate(raw_telemetry) + except ValidationError: + telemetry = None metadata = raw_metadata if isinstance(raw_metadata, dict) else None return simulation_params, telemetry, metadata diff --git a/libs/policyengine-simulation-observability/tests/test_errors.py b/libs/policyengine-simulation-observability/tests/test_errors.py index 08c133050..0b3301d4c 100644 --- a/libs/policyengine-simulation-observability/tests/test_errors.py +++ b/libs/policyengine-simulation-observability/tests/test_errors.py @@ -1,8 +1,9 @@ -"""Tests for gateway error-redaction helpers.""" +"""Tests for gateway error redaction.""" from __future__ import annotations import re +from unittest.mock import Mock from policyengine_simulation_observability import errors as errors_module @@ -10,117 +11,48 @@ CORRELATION_RE = re.compile(r"correlation_id=([0-9a-f]{32})") -def test_log_and_redact_exception_emits_correlation_id(monkeypatch): - class _FakeLogfire: - def __init__(self): - self.calls = [] +def test_log_and_redact_exception_records_runtime_event(caplog): + runtime = Mock() + exc = RuntimeError("signed URL containing secret material") - def exception(self, *args, **kwargs): - self.calls.append((args, kwargs)) - - recorded_errors = [] - recorded_events = [] - fake_logfire = _FakeLogfire() - monkeypatch.setattr( - errors_module, - "record_error", - lambda exc, **kwargs: recorded_errors.append((exc, kwargs)), - ) - monkeypatch.setattr( - errors_module, - "record_event", - lambda event, **kwargs: recorded_events.append((event, kwargs)), - ) - monkeypatch.setattr(errors_module, "_logfire", fake_logfire) - monkeypatch.setattr(errors_module, "logfire_is_configured", lambda: True) - - exc = RuntimeError( - "Signed GCS URL https://storage.googleapis.com/foo?token=SECRET " - "failed to resolve" - ) - message = errors_module.log_and_redact_exception( - exc, scope="test_scope", context={"job_id": "abc"} - ) - - match = CORRELATION_RE.search(message) - assert match is not None, message - - assert "SECRET" not in message - assert "token=" not in message - assert message.startswith("Simulation failed") - - assert recorded_errors == [ - ( + with caplog.at_level( + "ERROR", logger="policyengine_simulation_observability.errors" + ): + message = errors_module.log_and_redact_exception( exc, - { - "handled": True, - "status_code": 500, - }, + runtime=runtime, + scope="test_scope", + context={"job_id": "abc"}, ) - ] - assert len(recorded_events) == 1 - # Correlation id must appear in the server-side structured event. - event, kwargs = recorded_events[0] - assert event == "gateway_error_redacted" - assert kwargs["correlation_id"] == match.group(1) - assert kwargs["scope"] == "test_scope" - assert kwargs["job_id"] == "abc" - assert kwargs["error_type"] == "RuntimeError" - assert kwargs["logfire_status"] == "legacy_candidate_for_replacement" - assert kwargs["logfire_replacement_candidate"] == "policyengine-observability" - - assert len(fake_logfire.calls) == 1 - _, logfire_kwargs = fake_logfire.calls[0] - assert logfire_kwargs["correlation_id"] == match.group(1) - assert logfire_kwargs["scope"] == "test_scope" - assert logfire_kwargs["job_id"] == "abc" - assert logfire_kwargs["logfire_status"] == "legacy_candidate_for_replacement" - assert logfire_kwargs["logfire_replacement_candidate"] == ( - "policyengine-observability" - ) - - -def test_log_and_redact_exception_always_logs_to_stdlib(monkeypatch, caplog): - """The stdlib log line must fire even when every structured sink no-ops. - - record_error/record_event silently do nothing on a disabled runtime and - Logfire is unconfigured here, so without the unconditional stdlib log the - correlation id handed to the caller would point at nothing server-side. - """ - monkeypatch.setattr(errors_module, "record_error", lambda *a, **k: None) - monkeypatch.setattr(errors_module, "record_event", lambda *a, **k: None) - monkeypatch.setattr(errors_module, "logfire_is_configured", lambda: False) - exc = ValueError("secret-parameter-name") - with caplog.at_level("ERROR", logger="policyengine_simulation_observability.errors"): - message = errors_module.log_and_redact_exception(exc, scope="fallback") match = CORRELATION_RE.search(message) - assert match is not None, message - assert "secret-parameter-name" not in message - assert message.startswith("Simulation failed") - - # Exactly one guaranteed server-side record carrying the correlation id - # and the full exception (message + stack) for operators. - stdlib_records = [ - record for record in caplog.records if "fallback" in record.getMessage() - ] - assert len(stdlib_records) == 1 - record = stdlib_records[0] - assert match.group(1) in record.getMessage() - assert record.exc_info is not None - assert record.exc_info[1] is exc - - -def test_log_and_redact_exception_survives_structured_sink_failure(monkeypatch, caplog): - def _raise(*args, **kwargs): - raise RuntimeError("observability failed") - - monkeypatch.setattr(errors_module, "record_error", _raise) - monkeypatch.setattr(errors_module, "logfire_is_configured", lambda: False) - exc = ValueError("secret-parameter-name") - with caplog.at_level("ERROR", logger="policyengine_simulation_observability.errors"): - message = errors_module.log_and_redact_exception(exc, scope="fallback") + assert match is not None + assert "secret material" not in message + runtime.record_exception.assert_called_once_with(exc, handled=True, status_code=500) + attributes = runtime.event.call_args.kwargs["attributes"] + assert attributes == { + "correlation_id": match.group(1), + "scope": "test_scope", + "error_type": "RuntimeError", + "job_id": "abc", + } + assert any(match.group(1) in record.getMessage() for record in caplog.records) + + +def test_log_and_redact_exception_survives_runtime_failure(caplog): + runtime = Mock() + runtime.record_exception.side_effect = RuntimeError("export failed") + exc = ValueError("private parameter value") + + with caplog.at_level( + "ERROR", logger="policyengine_simulation_observability.errors" + ): + message = errors_module.log_and_redact_exception( + exc, + runtime=runtime, + scope="fallback", + ) - assert "secret-parameter-name" not in message + assert "private parameter value" not in message assert message.startswith("Simulation failed") - assert any("fallback" in record.message for record in caplog.records) + assert any("fallback" in record.getMessage() for record in caplog.records) diff --git a/libs/policyengine-simulation-observability/tests/test_logfire_legacy.py b/libs/policyengine-simulation-observability/tests/test_logfire_legacy.py deleted file mode 100644 index 07b8d1efd..000000000 --- a/libs/policyengine-simulation-observability/tests/test_logfire_legacy.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Tests for legacy Logfire helper behavior.""" - -from __future__ import annotations - -import sys -from contextlib import nullcontext - -import pytest - -from policyengine_simulation_observability import logfire_legacy - - -@pytest.fixture(autouse=True) -def _reset_logfire_configured_flag(monkeypatch): - """Isolate the module-level configured flag between tests.""" - monkeypatch.setattr(logfire_legacy, "_logfire_configured", False) - - -class _FakeLogfire: - def __init__(self) -> None: - self.configure_calls = [] - self.span_calls = [] - self.flush_calls = 0 - self.span_context = nullcontext("span") - - def configure(self, **kwargs): - self.configure_calls.append(kwargs) - - def span(self, name, **kwargs): - self.span_calls.append((name, kwargs)) - return self.span_context - - def force_flush(self): - self.flush_calls += 1 - - -def test_configure_logfire_skips_without_token(monkeypatch): - fake_logfire = _FakeLogfire() - monkeypatch.delenv("LOGFIRE_TOKEN", raising=False) - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - assert logfire_legacy.configure_logfire("policyengine-simulation") is False - assert fake_logfire.configure_calls == [] - - -def test_configure_logfire_uses_token_and_environment(monkeypatch): - fake_logfire = _FakeLogfire() - monkeypatch.setenv("LOGFIRE_TOKEN", "token-123") - monkeypatch.setenv("LOGFIRE_ENVIRONMENT", "staging") - monkeypatch.setenv("MODAL_ENVIRONMENT", "main") - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - assert logfire_legacy.configure_logfire("policyengine-simulation") is True - assert fake_logfire.configure_calls == [ - { - "service_name": "policyengine-simulation", - "token": "token-123", - "environment": "staging", - "console": False, - } - ] - - -def test_configure_logfire_falls_back_to_default_environment(monkeypatch): - fake_logfire = _FakeLogfire() - monkeypatch.setenv("LOGFIRE_TOKEN", "token-123") - monkeypatch.delenv("LOGFIRE_ENVIRONMENT", raising=False) - monkeypatch.setenv("MODAL_ENVIRONMENT", "main") - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - logfire_legacy.configure_logfire( - "policyengine-simulation", - default_environment="development", - ) - - assert fake_logfire.configure_calls[0]["environment"] == "development" - - -def test_logfire_is_configured_tracks_configure_state(monkeypatch): - """logfire_is_configured must reflect whether a token-backed configure ran. - - Logfire's own ``config.send_to_logfire`` defaults to True on an - unconfigured instance, so this module flag is the only reliable signal. - """ - fake_logfire = _FakeLogfire() - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - monkeypatch.delenv("LOGFIRE_TOKEN", raising=False) - assert logfire_legacy.configure_logfire("policyengine-simulation") is False - assert logfire_legacy.logfire_is_configured() is False - - monkeypatch.setenv("LOGFIRE_TOKEN", "token-123") - assert logfire_legacy.configure_logfire("policyengine-simulation") is True - assert logfire_legacy.logfire_is_configured() is True - - monkeypatch.delenv("LOGFIRE_TOKEN", raising=False) - assert logfire_legacy.configure_logfire("policyengine-simulation") is False - assert logfire_legacy.logfire_is_configured() is False - - -def test_logfire_span_noops_when_disabled(): - with logfire_legacy.logfire_span(False, "run_simulation") as span: - assert span is None - - -def test_logfire_span_delegates_when_enabled(monkeypatch): - fake_logfire = _FakeLogfire() - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - with logfire_legacy.logfire_span(True, "run_simulation", country="us") as span: - assert span == "span" - - assert fake_logfire.span_calls == [("run_simulation", {"country": "us"})] - - -def test_flush_logfire_only_flushes_when_enabled(monkeypatch): - fake_logfire = _FakeLogfire() - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - logfire_legacy.flush_logfire(False) - logfire_legacy.flush_logfire(True) - - assert fake_logfire.flush_calls == 1 diff --git a/libs/policyengine-simulation-observability/tests/test_observability.py b/libs/policyengine-simulation-observability/tests/test_observability.py index bc3c9a70d..2fdab665a 100644 --- a/libs/policyengine-simulation-observability/tests/test_observability.py +++ b/libs/policyengine-simulation-observability/tests/test_observability.py @@ -1,282 +1,176 @@ import inspect -import json -import os -import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from policyengine_observability import ( REQUEST_ID_HEADER, - ObservabilityRuntime, - operation, - set_observability_runtime, + GoogleCloudLogDestination, + StdoutLogDestination, +) +from policyengine_simulation_observability.identifiers import ( + OBSERVABILITY_ID_HEADER, ) -from policyengine_observability.runtime import OPERATION_LOGGER, REQUEST_LOGGER - from policyengine_simulation_observability.observability import ( - LOG_DESTINATIONS, - _environment, - configure_process_observability, + build_runtime, init_process_observability, init_simulation_observability, - process_static_attributes, -) - - -OBSERVABILITY_ENV_KEYS = ( - "OBSERVABILITY_ENVIRONMENT", - "OBSERVABILITY_PLATFORM", - "OBSERVABILITY_SERVICE_ROLE", - "OBSERVABILITY_RUNTIME_ROLE", - "OBSERVABILITY_MODAL_APP_NAME", - "OBSERVABILITY_MODAL_FUNCTION_NAME", - "OBSERVABILITY_LOG_DESTINATIONS", - "OTEL_ENABLED", - "GOOGLE_CLOUD_PROJECT", - "MODAL_ENVIRONMENT", - "DEPLOYMENT_ENVIRONMENT", - "APP_ENVIRONMENT", - "APP_ENV", - "ENVIRONMENT", + modal_image_environment, ) -@pytest.fixture(autouse=True) -def reset_observability_runtime(): - saved_env = {key: os.environ.get(key) for key in OBSERVABILITY_ENV_KEYS} - for key in OBSERVABILITY_ENV_KEYS: - os.environ.pop(key, None) - yield - set_observability_runtime(ObservabilityRuntime.disabled()) - for key, value in saved_env.items(): - if value is None: - os.environ.pop(key, None) - else: - os.environ[key] = value - - -def test_configure_process_observability_sets_modal_metadata(): - configure_process_observability( - platform="modal", +def test_modal_runtime_has_explicit_identity_and_remote_logging(monkeypatch): + monkeypatch.setenv("OTEL_SDK_DISABLED", "true") + monkeypatch.setenv("OTEL_TRACES_SAMPLER_ARG", "0.01") + monkeypatch.setenv("OBSERVABILITY_SERVICE_NAMESPACE", "example.stack") + monkeypatch.setenv("OBSERVABILITY_TRACE_PROJECT_ID", "trace-project") + monkeypatch.setenv("OBSERVABILITY_LOGGING_PROJECT_ID", "log-project") + monkeypatch.setenv("OBSERVABILITY_LOG_NAME", "simulation-modal") + runtime = build_runtime( + service_name="policyengine-simulation-py5-2-0", service_role="simulation_worker", - runtime_role="worker", - modal_app_name="policyengine-simulation-py4-19-1", - modal_function_name="run_simulation", - ) - - assert os.environ["OBSERVABILITY_PLATFORM"] == "modal" - assert os.environ["OBSERVABILITY_SERVICE_ROLE"] == "simulation_worker" - assert os.environ["OBSERVABILITY_RUNTIME_ROLE"] == "worker" - assert ( - os.environ["OBSERVABILITY_MODAL_APP_NAME"] == "policyengine-simulation-py4-19-1" - ) - assert os.environ["OBSERVABILITY_MODAL_FUNCTION_NAME"] == "run_simulation" - - -def test_configure_process_observability_overwrites_stale_metadata(): - os.environ["OBSERVABILITY_SERVICE_ROLE"] = "old_role" - os.environ["OBSERVABILITY_MODAL_FUNCTION_NAME"] = "old_function" - - configure_process_observability( platform="modal", - service_role="budget_window_worker", - modal_app_name="policyengine-simulation-py4-19-1", - modal_function_name="run_budget_window_batch", + environment="main", ) - - assert os.environ["OBSERVABILITY_SERVICE_ROLE"] == "budget_window_worker" - assert os.environ["OBSERVABILITY_RUNTIME_ROLE"] == "budget_window_worker" - assert os.environ["OBSERVABILITY_MODAL_FUNCTION_NAME"] == "run_budget_window_batch" - - -def test_process_static_attributes_carries_modal_identity(): - os.environ["MODAL_ENVIRONMENT"] = "main" - configure_process_observability( - platform="modal", - service_role="simulation_worker", - modal_app_name="policyengine-simulation-py4-19-1", - modal_function_name="run_simulation", - ) - - attributes = process_static_attributes(service_role="simulation_worker") - - assert attributes["platform"] == "modal" - assert attributes["runtime_role"] == "simulation_worker" - assert attributes["modal_environment"] == "main" - assert attributes["modal_app_name"] == "policyengine-simulation-py4-19-1" - assert attributes["modal_function_name"] == "run_simulation" - assert attributes["logfire_status"] == "legacy_candidate_for_replacement" - - -def test_worker_operation_log_includes_modal_identity(monkeypatch): - """Replicates the Modal worker entrypoint sequence: the operation log - must carry the Modal identity attributes, which have no FastAPI-adapter - injection point in plain-process runtimes.""" - configure_process_observability( - platform="modal", - service_role="simulation_worker", - modal_app_name="policyengine-simulation-py4-19-1", - modal_function_name="run_simulation", - ) - init_process_observability(service_role="simulation_worker") - - records = [] - monkeypatch.setattr( - OPERATION_LOGGER, - "info", - lambda message: records.append(json.loads(message)), - ) - - static_attributes = process_static_attributes(service_role="simulation_worker") - with operation("run_simulation", flavor="modal_function", **static_attributes): - pass - - assert len(records) == 1 - record = records[0] - assert record["event"] == "operation_completed" - assert record["operation"] == "run_simulation" - assert record["service_role"] == "simulation_worker" - assert record["platform"] == "modal" - assert record["runtime_role"] == "simulation_worker" - assert record["modal_app_name"] == "policyengine-simulation-py4-19-1" - assert record["modal_function_name"] == "run_simulation" - - -def test_init_simulation_observability_forces_stdout_and_disables_exports(): - os.environ["OTEL_ENABLED"] = "true" - os.environ["OBSERVABILITY_LOG_DESTINATIONS"] = "google_cloud_logging" - os.environ["GOOGLE_CLOUD_PROJECT"] = "policyengine-prod" - os.environ["MODAL_ENVIRONMENT"] = "staging" - - app = FastAPI() - runtime = init_simulation_observability( - app, - service_name="policyengine-simulation-gateway", - service_role="modal_gateway", + try: + assert runtime.config.service.name == "policyengine-simulation-py5-2-0" + assert runtime.config.service.namespace == "example.stack" + assert runtime.config.service.role == "simulation_worker" + assert runtime.config.deployment.environment == "main" + assert runtime.config.deployment.platform == "modal" + assert runtime.config.otel.sampling_ratio == 1.0 + stdout, remote = runtime.config.logging.destinations + assert isinstance(stdout, StdoutLogDestination) + assert isinstance(remote, GoogleCloudLogDestination) + assert remote.project_id == "log-project" + assert remote.log_name == "simulation-modal" + finally: + runtime.shutdown() + + +def test_cloud_run_runtime_uses_stdout_without_direct_remote_logging(monkeypatch): + monkeypatch.setenv("OTEL_SDK_DISABLED", "true") + monkeypatch.delenv("OBSERVABILITY_LOGGING_PROJECT_ID", raising=False) + monkeypatch.delenv("OBSERVABILITY_LOG_NAME", raising=False) + runtime = init_process_observability( + service_name="policyengine-simulation-entry-prod", + service_role="simulation_entry", + platform="google_cloud_run", + environment="prod", ) - - assert app.state.policyengine_observability is runtime - assert runtime.config.service_name == "policyengine-simulation-gateway" - assert runtime.config.service_role == "modal_gateway" - assert runtime.config.environment == "staging" - assert runtime.config.log_destinations == LOG_DESTINATIONS - assert runtime.config.otel_enabled is False - assert runtime.config.google_cloud_project is None - - -def test_fastapi_observability_emits_structured_request_log(monkeypatch): + try: + assert runtime.config.logging.capture_standard_library is True + assert len(runtime.config.logging.destinations) == 1 + assert isinstance(runtime.config.logging.destinations[0], StdoutLogDestination) + finally: + runtime.shutdown() + + +def test_modal_image_environment_only_copies_explicit_destinations(monkeypatch): + for name in ( + "OBSERVABILITY_LOGGING_PROJECT_ID", + "OBSERVABILITY_LOG_NAME", + "OTEL_EXPORTER_OTLP_ENDPOINT", + ): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("OBSERVABILITY_LOGGING_PROJECT_ID", "another-project") + monkeypatch.setenv("OBSERVABILITY_LOG_NAME", "another-log") + + environment = modal_image_environment() + + assert environment["OBSERVABILITY_LOGGING_PROJECT_ID"] == "another-project" + assert environment["OBSERVABILITY_LOG_NAME"] == "another-log" + assert "OTEL_EXPORTER_OTLP_ENDPOINT" not in environment + assert environment["OTEL_EXPORTER_OTLP_PROTOCOL"] == "grpc" + assert environment["OTEL_TRACES_SAMPLER_ARG"] == "1.0" + + +def test_fastapi_adapter_preserves_response_and_request_id(monkeypatch): + monkeypatch.setenv("OTEL_SDK_DISABLED", "true") app = FastAPI() @app.get("/health") def health(): return {"status": "healthy"} - init_simulation_observability( + runtime = init_simulation_observability( app, - service_name="policyengine-simulation-entry", - service_role="api", - ) - - records = [] - monkeypatch.setattr( - REQUEST_LOGGER, - "info", - lambda message: records.append(json.loads(message)), - ) - - response = TestClient(app).get( - "/health", - headers={REQUEST_ID_HEADER: "request-123"}, + service_name="policyengine-simulation-entry-prod", + service_role="simulation_entry", + platform="google_cloud_run", + environment="prod", ) - - assert response.status_code == 200 - assert len(records) == 1 - record = records[0] - assert record["schema_version"] == "policyengine.observability.request.v1" - assert record["event"] == "http_request_completed" - assert record["service_name"] == "policyengine-simulation-entry" - assert record["service_role"] == "api" - assert record["request_id"] == "request-123" - assert record["method"] == "GET" - assert record["route"] == "/health" - assert record["status_code"] == 200 - assert record["logfire_status"] == "legacy_candidate_for_replacement" - assert record["logfire_replacement_candidate"] == "policyengine-observability" - - -def test_fastapi_service_name_is_a_required_keyword_only_argument(): - parameter = inspect.signature(init_simulation_observability).parameters[ - "service_name" - ] - - assert parameter.kind is inspect.Parameter.KEYWORD_ONLY - assert parameter.default is inspect.Parameter.empty - - -@pytest.mark.parametrize("app_environment", ["beta", "prod"]) -def test_app_environment_configures_cloud_run_runtime(app_environment): - os.environ["APP_ENVIRONMENT"] = app_environment - + try: + response = TestClient(app).get( + "/health", + headers={REQUEST_ID_HEADER: "request-123"}, + ) + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + assert response.headers[REQUEST_ID_HEADER] == "request-123" + assert app.state.policyengine_observability is runtime + finally: + runtime.shutdown() + + +def test_fastapi_adapter_attaches_observability_id_to_active_request(monkeypatch): + monkeypatch.setenv("OTEL_SDK_DISABLED", "true") app = FastAPI() runtime = init_simulation_observability( app, - service_name="policyengine-simulation-entry", + service_name="policyengine-simulation-entry-prod", service_role="simulation_entry", + platform="google_cloud_run", + environment="prod", ) - assert runtime.config.environment == app_environment - - -@pytest.mark.parametrize( - ("environment", "expected"), - [ - ( - { - "OBSERVABILITY_ENVIRONMENT": "observability", - "MODAL_ENVIRONMENT": "modal", - "DEPLOYMENT_ENVIRONMENT": "deployment", - "APP_ENVIRONMENT": "app-environment", - "APP_ENV": "app-env", - "ENVIRONMENT": "generic", - }, - "observability", - ), - ( - { - "MODAL_ENVIRONMENT": "modal", - "DEPLOYMENT_ENVIRONMENT": "deployment", - "APP_ENVIRONMENT": "app-environment", - "APP_ENV": "app-env", - "ENVIRONMENT": "generic", - }, - "modal", - ), - ( - { - "DEPLOYMENT_ENVIRONMENT": "deployment", - "APP_ENVIRONMENT": "app-environment", - "APP_ENV": "app-env", - "ENVIRONMENT": "generic", + @app.get("/context") + def context(): + return runtime.capture_context() + + observability_id = "00000000-0000-4000-8000-000000000001" + try: + response = TestClient(app).get( + "/context", + headers={ + REQUEST_ID_HEADER: "request-123", + OBSERVABILITY_ID_HEADER: observability_id, }, - "deployment", - ), - ( - { - "APP_ENVIRONMENT": "app-environment", - "APP_ENV": "app-env", - "ENVIRONMENT": "generic", - }, - "app-environment", - ), - ( - {"APP_ENV": "app-env", "ENVIRONMENT": "generic"}, - "app-env", - ), - ({"ENVIRONMENT": "generic"}, "generic"), - ({}, "local"), - ], -) -def test_environment_precedence(environment, expected): - os.environ.update(environment) + ) + assert response.status_code == 200 + assert response.json()["request_id"] == "request-123" + assert response.json()["observability_id"] == observability_id + finally: + runtime.shutdown() + + +def test_observability_setup_failure_does_not_change_fastapi_response( + monkeypatch, +): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://invalid.invalid") + monkeypatch.setenv("POLICYENGINE_OTEL_GOOGLE_AUDIENCE", "not-a-url") + app = FastAPI() + + @app.get("/result") + def result(): + return {"result": 42} - assert _environment() == expected + runtime = init_simulation_observability( + app, + service_name="policyengine-simulation-entry-prod", + service_role="simulation_entry", + platform="google_cloud_run", + environment="prod", + ) + try: + response = TestClient(app).get("/result") + assert response.status_code == 200 + assert response.json() == {"result": 42} + finally: + runtime.shutdown() + + +def test_service_identity_arguments_are_required_and_keyword_only(): + parameters = inspect.signature(init_simulation_observability).parameters + for name in ("service_name", "service_role", "platform", "environment"): + assert parameters[name].kind is inspect.Parameter.KEYWORD_ONLY + assert parameters[name].default is inspect.Parameter.empty diff --git a/libs/policyengine-simulation-observability/tests/test_stages.py b/libs/policyengine-simulation-observability/tests/test_stages.py new file mode 100644 index 000000000..d6203e503 --- /dev/null +++ b/libs/policyengine-simulation-observability/tests/test_stages.py @@ -0,0 +1,31 @@ +import pytest + +from policyengine_simulation_observability.stages import ( + RUN_STAGE_REGISTRY, + RunConfiguration, + Stage, +) + + +def test_registry_defines_every_run_configuration_and_stage() -> None: + assert set(RUN_STAGE_REGISTRY) == set(RunConfiguration) + registered = { + stage + for stage_plan in RUN_STAGE_REGISTRY.values() + for stage in stage_plan.stages + } + assert registered == set(Stage) + + +@pytest.mark.parametrize("stage_plan", RUN_STAGE_REGISTRY.values()) +def test_each_stage_plan_is_ordered_and_contains_no_duplicates(stage_plan) -> None: + assert stage_plan.stages + assert len(stage_plan.stages) == len(set(stage_plan.stages)) + assert all(stage_plan.name(stage) == stage.value for stage in stage_plan.stages) + + +def test_stage_plan_rejects_a_stage_from_another_configuration() -> None: + annual = RUN_STAGE_REGISTRY[RunConfiguration.ANNUAL_IMPACT] + + with pytest.raises(ValueError, match="not registered"): + annual.name(Stage.STAGE12_AGGREGATION) diff --git a/libs/policyengine-simulation-observability/tests/test_telemetry.py b/libs/policyengine-simulation-observability/tests/test_telemetry.py index 80d312d08..96b470253 100644 --- a/libs/policyengine-simulation-observability/tests/test_telemetry.py +++ b/libs/policyengine-simulation-observability/tests/test_telemetry.py @@ -1,5 +1,8 @@ +import pytest from policyengine_simulation_observability.telemetry import ( TelemetryEnvelope, + apply_remote_context, + remote_context, split_internal_payload, ) @@ -8,25 +11,97 @@ def test_split_internal_payload__removes_internal_fields(): payload = { "country": "us", "scope": "macro", - "_metadata": {"process_id": "proc-123"}, + "_metadata": {"submission_claim_id": "proc-123"}, "_telemetry": { - "run_id": "run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, + "_observability_context": { + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "captured_at": "2026-09-22T00:00:00Z", + "request_id": "request-123", + }, } simulation_params, telemetry, metadata = split_internal_payload(payload) assert "_metadata" not in simulation_params assert "_telemetry" not in simulation_params + assert "_observability_context" not in simulation_params assert simulation_params == {"country": "us", "scope": "macro"} assert telemetry == TelemetryEnvelope( - run_id="run-123", - process_id="proc-123", + submission_claim_id="proc-123", capture_mode="disabled", ) - assert metadata == {"process_id": "proc-123"} + assert metadata == {"submission_claim_id": "proc-123"} + + +def test_remote_context_validates_and_serializes_allowlisted_fields(): + context = remote_context( + { + "_observability_context": { + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "captured_at": "2026-09-22T00:00:00Z", + "request_id": "request-123", + "job_id": "job-123", + } + } + ) + + assert context is not None + assert context["request_id"] == "request-123" + assert context["job_id"] == "job-123" + assert context["captured_at"] == "2026-09-22 00:00:00+00:00" + + +def test_remote_context_rejects_malformed_or_extra_values(): + assert remote_context({"_observability_context": {"captured_at": "bad"}}) is None + assert ( + remote_context( + { + "_observability_context": { + "captured_at": "2026-09-22T00:00:00Z", + "observability_id": "not-a-uuid", + } + } + ) + is None + ) + assert ( + remote_context( + { + "_observability_context": { + "captured_at": "2026-09-22T00:00:00Z", + "household": {"people": {}}, + } + } + ) + is None + ) + + +def test_apply_remote_context_sets_the_runtime_diagnostic_identifier(): + class Runtime: + def __init__(self): + self.context = {} + + def set_context(self, **values): + self.context.update(values) + + runtime = Runtime() + params = { + "_observability_context": { + "captured_at": "2026-09-22T00:00:00Z", + "observability_id": "00000000-0000-4000-8000-000000000001", + } + } + + propagated = apply_remote_context(runtime, params) + + assert propagated is not None + assert runtime.context == { + "observability_id": "00000000-0000-4000-8000-000000000001" + } def test_split_internal_payload__tolerates_missing_internal_fields(): @@ -37,3 +112,47 @@ def test_split_internal_payload__tolerates_missing_internal_fields(): assert simulation_params == payload assert telemetry is None assert metadata is None + + +def test_split_internal_payload__drops_malformed_telemetry_without_failing_work(): + payload = { + "country": "us", + "scope": "macro", + "_telemetry": {"capture_mode": "not-supported"}, + } + + simulation_params, telemetry, metadata = split_internal_payload(payload) + + assert simulation_params == {"country": "us", "scope": "macro"} + assert telemetry is None + assert metadata is None + + +@pytest.mark.parametrize( + "field", ["observability_id", "run_id", "request_id", "traceparent"] +) +def test_telemetry_discards_noncanonical_context_fields(field): + telemetry = TelemetryEnvelope.model_validate( + {field: "identifier", "submission_claim_id": "claim-123"} + ) + + assert telemetry.submission_claim_id == "claim-123" + assert field not in telemetry.model_dump() + + +def test_telemetry_maps_legacy_process_id_to_submission_claim_id(): + telemetry = TelemetryEnvelope.model_validate({"process_id": "legacy-claim-123"}) + + assert telemetry.submission_claim_id == "legacy-claim-123" + assert "process_id" not in telemetry.model_dump() + + +def test_telemetry_prefers_canonical_submission_claim_id_over_legacy_value(): + telemetry = TelemetryEnvelope.model_validate( + { + "process_id": "legacy-claim-123", + "submission_claim_id": "canonical-claim-456", + } + ) + + assert telemetry.submission_claim_id == "canonical-claim-456" diff --git a/libs/policyengine-simulation-observability/uv.lock b/libs/policyengine-simulation-observability/uv.lock index fcf511e26..f956d0ae9 100644 --- a/libs/policyengine-simulation-observability/uv.lock +++ b/libs/policyengine-simulation-observability/uv.lock @@ -36,15 +36,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] -[[package]] -name = "asgiref" -version = "3.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, -] - [[package]] name = "black" version = "26.5.1" @@ -81,6 +72,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -214,12 +278,53 @@ wheels = [ ] [[package]] -name = "executing" -version = "2.2.1" +name = "cryptography" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, ] [[package]] @@ -239,341 +344,342 @@ wheels = [ ] [[package]] -name = "googleapis-common-protos" -version = "1.75.0" +name = "google-api-core" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, { name = "protobuf" }, + { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e5/18aeff14213db86267a0f79d869352954394ab4ab3a3866d9b9b0e82dc6a/google_api_core-2.38.0.tar.gz", hash = "sha256:31e326eafa31b34f1db7a715f50f61dbca7e6e37277762f252ed90e6ce94c246", size = 206784, upload-time = "2026-09-17T20:30:08.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/18/55/e545e4b0040cb812f65d2abc545c6420f57b687d2af130a447c86d14d00b/google_api_core-2.38.0-py3-none-any.whl", hash = "sha256:8db2730375eba434bd75041fbe06b5374287407689ce132ac5650c8c8f042fcc", size = 187996, upload-time = "2026-09-17T20:29:32.813Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, ] [[package]] -name = "grpcio" -version = "1.81.1" +name = "google-auth" +version = "2.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "cryptography" }, + { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, - { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, - { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, ] [[package]] -name = "h11" -version = "0.16.0" +name = "google-cloud-appengine-logging" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, ] [[package]] -name = "httpcore2" -version = "2.5.0" +name = "google-cloud-audit-log" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "googleapis-common-protos" }, + { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/06/5c12df521b5322fb1114a83d46911b2fbcb8855ddb3a635f11c01a214af5/httpcore2-2.5.0.tar.gz", hash = "sha256:88aa170137c17328d5ac44234f9fd10706466d5fb347f3edac4d39b91137b09d", size = 64808, upload-time = "2026-06-25T14:16:56.472Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, ] [[package]] -name = "httpx2" -version = "2.5.0" +name = "google-cloud-core" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "httpcore2" }, - { name = "idna" }, - { name = "truststore" }, + { name = "google-api-core" }, + { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/e2/b5dedc0cf35aa65de5f541ccd30d2bc1fd7f1d43c9ab09f8ed9a7342317b/httpx2-2.5.0.tar.gz", hash = "sha256:e2df9cb4611021527ff8a675b1c320b610a2ec397acc8d6fe6e91df2d9b33c29", size = 83121, upload-time = "2026-06-25T14:16:57.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl", hash = "sha256:3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41", size = 76652, upload-time = "2026-06-25T14:16:55.23Z" }, + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, ] [[package]] -name = "idna" -version = "3.18" +name = "google-cloud-logging" +version = "3.16.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, ] [[package]] -name = "importlib-metadata" -version = "9.0.0" +name = "googleapis-common-protos" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, ] [[package]] -name = "logfire" -version = "4.37.0" +name = "grpc-google-iam-v1" +version = "0.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/f2/34b8ebbd6bbd82c71055d6b881b24d8ada79a0e6692d3dd8cca5e86fadb3/logfire-4.37.0.tar.gz", hash = "sha256:7ee0cb64b59c356a41a1701fb84597037f8db1fa15df7a3715ef363e5a1de06a", size = 1212176, upload-time = "2026-06-12T20:47:06.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/08/1805d2f26955671115aae555d78cc4c72a6fe733f332d44d69756bc1737b/logfire-4.37.0-py3-none-any.whl", hash = "sha256:a20823e6dbb3204614a3ea5e79c91df42405c5112393ec9d8e34ef45b60d315f", size = 378930, upload-time = "2026-06-12T20:47:03.674Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, ] [[package]] -name = "markdown-it-py" -version = "4.2.0" +name = "grpcio" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] [[package]] -name = "mdurl" -version = "0.1.2" +name = "grpcio-status" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] -name = "mypy-extensions" -version = "1.1.0" +name = "h11" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] -name = "nodeenv" -version = "1.10.0" +name = "httpcore2" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/06/5c12df521b5322fb1114a83d46911b2fbcb8855ddb3a635f11c01a214af5/httpcore2-2.5.0.tar.gz", hash = "sha256:88aa170137c17328d5ac44234f9fd10706466d5fb347f3edac4d39b91137b09d", size = 64808, upload-time = "2026-06-25T14:16:56.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" }, ] [[package]] -name = "opentelemetry-api" -version = "1.42.1" +name = "httpx2" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/e2/b5dedc0cf35aa65de5f541ccd30d2bc1fd7f1d43c9ab09f8ed9a7342317b/httpx2-2.5.0.tar.gz", hash = "sha256:e2df9cb4611021527ff8a675b1c320b610a2ec397acc8d6fe6e91df2d9b33c29", size = 83121, upload-time = "2026-06-25T14:16:57.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl", hash = "sha256:3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41", size = 76652, upload-time = "2026-06-25T14:16:55.23Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.42.1" +name = "idna" +version = "3.18" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.42.1" +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.42.1" +name = "mypy-extensions" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] -name = "opentelemetry-instrumentation" -version = "0.63b1" +name = "nodeenv" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.63b1" +name = "opentelemetry-api" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/7e/83986f27b421de04fab1e1a84e892621dac42e6432a9c66779505f4d1381/opentelemetry_instrumentation_asgi-0.63b1-py3-none-any.whl", hash = "sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e", size = 15906, upload-time = "2026-05-21T16:35:04.162Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.63b1" +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/d6/0c128fac2e34b7d526a8d3c6edc45b875a97f8a987861b00511151b6337d/opentelemetry_instrumentation_fastapi-0.63b1.tar.gz", hash = "sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba", size = 25387, upload-time = "2026-05-21T16:36:32.071Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/3d/2eae63f13f36d7a8ab5bf03d06ecaf169c2069b524547f24947be6d92094/opentelemetry_instrumentation_fastapi-0.63b1-py3-none-any.whl", hash = "sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281", size = 12795, upload-time = "2026-05-21T16:35:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.63b1" +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/27/c2b4335bca030e893acbe5ff2b4f434868773bf94508be7e6bf5af981b24/opentelemetry_instrumentation_httpx-0.63b1.tar.gz", hash = "sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4", size = 23557, upload-time = "2026-05-21T16:36:34.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/b8/f536780996195c3b9f2354998554671e05a7a262df8c043f63fe9e5a6f0b/opentelemetry_instrumentation_httpx-0.63b1-py3-none-any.whl", hash = "sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86", size = 16336, upload-time = "2026-05-21T16:35:32.221Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.63b1" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/d8/7bf5e4cec0578ac3c28c18eb7b88f34279139cbc8c568d6aa02b9c5ae53e/opentelemetry_util_http-0.63b1.tar.gz", hash = "sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff", size = 11102, upload-time = "2026-05-21T16:36:56.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/f1/34e047e8f6a3c67e5220acf1af7b9f62868c25d77791bca74457bd2180a6/opentelemetry_util_http-0.63b1-py3-none-any.whl", hash = "sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8", size = 8205, upload-time = "2026-05-21T16:36:09.736Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] @@ -614,25 +720,26 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "1.3.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2a/5e0e4c0f30b20a59ffff14364e15374f9eee494f658f41559aba97c152a2/policyengine_observability-1.3.0.tar.gz", hash = "sha256:4c2f3bb2ee59886aef6c90f1edb9c0b957ace75b1518616c0e61905b8be431e0", size = 107961, upload-time = "2026-07-01T21:10:53.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/a2/80e4bc1c4923e8f4c04483bfd22df2500b0d132c4ce1a6dec6749c94d947/policyengine_observability-1.3.0-py3-none-any.whl", hash = "sha256:7a9444e2cb8ddd7f6d54d601bd1e0a7651bd44783b78627b293a27e741f91930", size = 31087, upload-time = "2026-07-01T21:10:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-observability" @@ -640,9 +747,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -663,9 +768,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -674,6 +777,18 @@ requires-dist = [ ] provides-extras = ["test", "build"] +[[package]] +name = "proto-plus" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -689,6 +804,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -863,19 +1008,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - [[package]] name = "starlette" version = "1.3.1" @@ -926,65 +1058,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] - -[[package]] -name = "wrapt" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, - { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, - { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, - { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, - { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, - { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, - { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, -] - -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -] diff --git a/libs/policyengine-stage12-persistence/src/policyengine_stage12_persistence/tables.py b/libs/policyengine-stage12-persistence/src/policyengine_stage12_persistence/tables.py index 3a889f86d..d3169fa02 100644 --- a/libs/policyengine-stage12-persistence/src/policyengine_stage12_persistence/tables.py +++ b/libs/policyengine-stage12-persistence/src/policyengine_stage12_persistence/tables.py @@ -92,6 +92,7 @@ Column("environment", String(255), nullable=False), Column("calculation_flow", String(255), nullable=False), Column("originating_request_id", String(255), nullable=False), + Column("observability_id", String(36)), Column("production_identity", String(255), nullable=False), Column("incumbent_execution_id", String(255)), Column("worker_version", String(255), nullable=False), diff --git a/libs/policyengine-stage12-persistence/tests/test_store.py b/libs/policyengine-stage12-persistence/tests/test_store.py index ec7ecec2c..ebf4f060d 100644 --- a/libs/policyengine-stage12-persistence/tests/test_store.py +++ b/libs/policyengine-stage12-persistence/tests/test_store.py @@ -19,6 +19,7 @@ NOW = datetime(2026, 9, 22, tzinfo=UTC) REPORT_ID = UUID("00000000-0000-0000-0000-000000000001") SIMULATION_ID = UUID("00000000-0000-0000-0000-000000000002") +OBSERVABILITY_ID = "00000000-0000-4000-8000-000000000012" def _report() -> ComparisonReportRecord: @@ -29,6 +30,7 @@ def _report() -> ComparisonReportRecord: environment="staging", calculation_flow="economy", originating_request_id="request-1", + observability_id=OBSERVABILITY_ID, production_identity="job-1", incumbent_execution_id="job-1", worker_version="5.2.0", @@ -124,6 +126,7 @@ def test_create_and_read_report_use_sqlalchemy_statements_only() -> None: assert result.created is True assert result.record == report + assert result.record.observability_id == OBSERVABILITY_ID assert len(engine.connection.statements) == 1 diff --git a/libs/policyengine-stage12-persistence/uv.lock b/libs/policyengine-stage12-persistence/uv.lock index 903cec592..dbb98ebc3 100644 --- a/libs/policyengine-stage12-persistence/uv.lock +++ b/libs/policyengine-stage12-persistence/uv.lock @@ -33,15 +33,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] -[[package]] -name = "asgiref" -version = "3.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, -] - [[package]] name = "black" version = "26.5.1" @@ -73,6 +64,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, +] + [[package]] name = "charset-normalizer" version = "3.5.1" @@ -162,12 +178,40 @@ wheels = [ ] [[package]] -name = "executing" -version = "2.2.1" +name = "cryptography" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, ] [[package]] @@ -186,6 +230,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] +[[package]] +name = "google-api-core" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/e5/18aeff14213db86267a0f79d869352954394ab4ab3a3866d9b9b0e82dc6a/google_api_core-2.38.0.tar.gz", hash = "sha256:31e326eafa31b34f1db7a715f50f61dbca7e6e37277762f252ed90e6ce94c246", size = 206784, upload-time = "2026-09-17T20:30:08.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/55/e545e4b0040cb812f65d2abc545c6420f57b687d2af130a447c86d14d00b/google_api_core-2.38.0-py3-none-any.whl", hash = "sha256:8db2730375eba434bd75041fbe06b5374287407689ce132ac5650c8c8f042fcc", size = 187996, upload-time = "2026-09-17T20:29:32.813Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, +] + +[[package]] +name = "google-cloud-appengine-logging" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, +] + +[[package]] +name = "google-cloud-logging" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.3" @@ -198,6 +341,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" }, ] +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "greenlet" version = "3.5.6" @@ -214,6 +362,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, ] +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, +] + [[package]] name = "grpcio" version = "1.84.0" @@ -236,24 +398,26 @@ wheels = [ ] [[package]] -name = "idna" -version = "3.20" +name = "grpcio-status" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] -name = "importlib-metadata" -version = "9.0.1" +name = "idna" +version = "3.20" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -265,45 +429,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "logfire" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/da/0c73a4447dffe01e1737580556f5f061ffda8f85e46fbf35ac36f5b64d27/logfire-5.1.0.tar.gz", hash = "sha256:edf6c7d33c00214e11c2cdc567eb53051be36289747091e8bb910527e5970bea", size = 1359738, upload-time = "2026-09-11T10:25:11.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/02/b32c3d5c298e4a9010694d261fb82c2c672747c0bf2b47bffda484aef349/logfire-5.1.0-py3-none-any.whl", hash = "sha256:c1d54eabf39f4c8f5e18e71f35da13375c8adb144c263f3076e650ba85929108", size = 474841, upload-time = "2026-09-11T10:25:08.131Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -364,87 +489,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/03/a529140241addd4d0acc73bafbd6f74691651b92fc0ae9b4513cf80f07fa/opentelemetry_instrumentation_httpx-0.65b0.tar.gz", hash = "sha256:4627aa9c6bb99bf4462c8b565b0ef6aeb9ffad95c6c92868be1ef7895de112ee", size = 26309, upload-time = "2026-07-16T15:26:07.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/0f/c6144096b4914bbf44b43ba21c962e8f333ff045770b50a3e79ed8bd455f/opentelemetry_instrumentation_httpx-0.65b0-py3-none-any.whl", hash = "sha256:400f1b78afa4ee2332b5debe58e1ed1b317913d58812c952576be76660aeadb1", size = 17436, upload-time = "2026-07-16T15:25:15.772Z" }, -] - [[package]] name = "opentelemetry-proto" version = "1.44.0" @@ -484,15 +528,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] -[[package]] -name = "opentelemetry-util-http" -version = "0.65b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, -] - [[package]] name = "packaging" version = "26.3" @@ -531,25 +566,26 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "1.4.1" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/12/8d2860d24f2d2169aafde9c623c62075d3b6cbf37143a2bf497ca4feaa00/policyengine_observability-1.4.1.tar.gz", hash = "sha256:454bf36da0a1f20726406e17ff2c5e61e554d1db72e021e97439a66884befb78", size = 136942, upload-time = "2026-09-17T16:30:33.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/5c/78b39da0e69e1b6604fb9f911867e6e9d6e12500d28b73f6ce76ee296637/policyengine_observability-1.4.1-py3-none-any.whl", hash = "sha256:31972446a2270689efb26d9dd45d2eda344fd03f74fca5511adb02982e0430f6", size = 52245, upload-time = "2026-09-17T16:30:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-contract" @@ -582,9 +618,7 @@ version = "0.1.0" source = { editable = "../policyengine-simulation-observability" } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -593,9 +627,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -636,6 +668,18 @@ requires-dist = [ ] provides-extras = ["test", "build"] +[[package]] +name = "proto-plus" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, +] + [[package]] name = "protobuf" version = "7.36.2" @@ -686,6 +730,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/7f/e350e1cf498ba2565c3f87b12f429d2012eb86b76c2b3845a19ee5fbb4d6/psycopg_binary-3.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:0ebfad5d131de9f892ae9e70cc7616207768b6714b66a52d4612b8ceaf78b372", size = 3658179, upload-time = "2026-09-18T13:20:22.691Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.13.5" @@ -808,19 +882,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.54" @@ -891,32 +952,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909 wheels = [ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] - -[[package]] -name = "wrapt" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/a6/6375d56c44d590ef24acf0f8f5bf7ed768ff7a510b959306ec412611e90f/wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16", size = 164597, upload-time = "2026-09-10T23:12:16.811Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/1a/9b5aa3c2391aa6d00fffa085c2219e8fc91cd4d2b9b080d2b4e4b6b93f42/wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3", size = 98597, upload-time = "2026-09-10T23:10:23.723Z" }, - { url = "https://files.pythonhosted.org/packages/fe/07/dc98150c2f9ee5b5fcbd841765e178ea6cc6c43733ab8d1f5181a4fb9f3d/wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145", size = 98842, upload-time = "2026-09-10T23:10:25.109Z" }, - { url = "https://files.pythonhosted.org/packages/08/c2/0e772a570e8d75c1b3ec45930a3023492fa68223f8f5e3485af4844c10c3/wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc", size = 234642, upload-time = "2026-09-10T23:10:26.644Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/4ce4d02dd95ff9ed972a2fc396d04fec636daa5a4ac18cc73a3dc20aa6c3/wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1", size = 235484, upload-time = "2026-09-10T23:10:28.098Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/436ef1df620ef8edd9ef057b4d55a0cd704435ff66852a0ef26cbaa02a58/wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d", size = 214371, upload-time = "2026-09-10T23:10:29.841Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2c/cc5b7503843399087db3106a7cf60d60d0900f69539e96148b9fff116291/wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697", size = 232347, upload-time = "2026-09-10T23:10:31.511Z" }, - { url = "https://files.pythonhosted.org/packages/86/fb/17668b1ca572c44b458c09d76064d35f5f57d5ac7629248871604bef816c/wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73", size = 212792, upload-time = "2026-09-10T23:10:32.968Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2b/0c5de10d7259e07c478c44bf2fbe179c6878727d09edf0632b97884801db/wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4", size = 223585, upload-time = "2026-09-10T23:10:34.43Z" }, - { url = "https://files.pythonhosted.org/packages/aa/36/013dce1c687f8f1c87b1e78f403d04c80ae9b2ae77de4ddba16069a3e7d1/wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4", size = 93425, upload-time = "2026-09-10T23:10:35.901Z" }, - { url = "https://files.pythonhosted.org/packages/b7/1d/d53bcf5910209a45191c8bc173ff0e00830416547d8038772dc444932ee0/wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca", size = 98569, upload-time = "2026-09-10T23:10:37.316Z" }, - { url = "https://files.pythonhosted.org/packages/e8/1f/759d0c522918f9dfb620136622d8e1ce619570adda0de8fa2cfbb55cbb86/wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f", size = 95272, upload-time = "2026-09-10T23:10:38.657Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a2/edcfc8d9a30375791b775715f8501364588f65b494ec4f6930568a19e765/wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c", size = 75317, upload-time = "2026-09-10T23:12:14.82Z" }, -] - -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -] diff --git a/projects/policyengine-simulation-entry/pyproject.toml b/projects/policyengine-simulation-entry/pyproject.toml index 3f42f6d11..804a67430 100644 --- a/projects/policyengine-simulation-entry/pyproject.toml +++ b/projects/policyengine-simulation-entry/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "fastapi>=0.115.0,<1", "httpx>=0.28.0,<1", "modal>=1.4,<2", - "policyengine-observability[fastapi]>=1.3.0,<2", + "policyengine-observability[fastapi,google,httpx,otlp-grpc]>=3,<4", "policyengine-simulation-contract", "policyengine-simulation-observability", "policyengine-stage12-persistence", diff --git a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/app.py b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/app.py index 7515cdd40..76d9cd26d 100644 --- a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/app.py +++ b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/app.py @@ -13,7 +13,7 @@ from fastapi import Depends, FastAPI, Request from fastapi.responses import JSONResponse, Response -from policyengine_observability import REQUEST_ID_HEADER, record_event +from policyengine_observability import REQUEST_ID_HEADER from policyengine_simulation_contract.gateway_models import ( BudgetWindowBatchRequest, BudgetWindowBatchStatusResponse, @@ -35,8 +35,11 @@ ComparisonRunLifecycleStatus, ComparisonSimulationRecord, ) +from policyengine_simulation_observability.identifiers import ( + OBSERVABILITY_ID_HEADER, + resolve_observability_id, +) from policyengine_simulation_observability.observability import ( - configure_process_observability, init_simulation_observability, ) from pydantic import BaseModel, TypeAdapter, ValidationError @@ -81,6 +84,7 @@ async def dispatch_after_production( request_payload: dict[str, Any], production_response: bytes, request_id: str, + observability_id: str, ) -> None: ... async def submit_temporary_report( @@ -88,6 +92,7 @@ async def submit_temporary_report( *, request_payload: dict[str, Any], request_id: str, + observability_id: str, ) -> ComparisonReportRecord: ... async def get_temporary_report( @@ -146,15 +151,6 @@ def create_app( """Build the app with injectable auth/backend seams for hermetic tests.""" runtime_settings = settings or Settings.from_env() - runtime_backend = backend or OldGatewayBackend(runtime_settings) - authenticate = auth_dependency or CallerAuthenticator(runtime_settings) - runtime_comparison = comparison_backend - if runtime_comparison is None and runtime_settings.stage12_resources_configured: - from policyengine_simulation_entry.stage12_backend import ( - Stage12ComparisonBackend, - ) - - runtime_comparison = Stage12ComparisonBackend.from_settings(runtime_settings) @asynccontextmanager async def lifespan(_: FastAPI): @@ -164,6 +160,7 @@ async def lifespan(_: FastAPI): yield finally: await runtime_backend.close() + runtime.shutdown() app = FastAPI( title="PolicyEngine Simulation Entrypoint", @@ -172,23 +169,37 @@ async def lifespan(_: FastAPI): lifespan=lifespan, ) - configure_process_observability( - platform="cloud_run", - service_role="simulation_entry", - ) - init_simulation_observability( + runtime = init_simulation_observability( app, service_name="policyengine-simulation-entry", service_role="simulation_entry", + platform="google_cloud_run", + environment=runtime_settings.environment, ) + runtime_backend = backend or OldGatewayBackend(runtime_settings, runtime=runtime) + authenticate = auth_dependency or CallerAuthenticator(runtime_settings, runtime) + runtime_comparison = comparison_backend + if runtime_comparison is None and runtime_settings.stage12_resources_configured: + from policyengine_simulation_entry.stage12_backend import ( + Stage12ComparisonBackend, + ) + + runtime_comparison = Stage12ComparisonBackend.from_settings( + runtime_settings, + runtime=runtime, + ) @app.middleware("http") async def request_context(request: Request, call_next): headers = MutableHeaders(scope=request.scope) request_id = headers.get(REQUEST_ID_HEADER) or str(uuid.uuid4()) + observability_id = resolve_observability_id( + headers.get(OBSERVABILITY_ID_HEADER) + ) headers[REQUEST_ID_HEADER] = request_id + headers[OBSERVABILITY_ID_HEADER] = observability_id request.state.request_id = request_id - started = time.monotonic() + request.state.observability_id = observability_id try: response = await call_next(request) except Exception: @@ -207,23 +218,12 @@ async def request_context(request: Request, call_next): media_type="text/plain", headers={REQUEST_ID_HEADER: request_id}, ) - elapsed_ms = round((time.monotonic() - started) * 1000, 2) + if OBSERVABILITY_ID_HEADER not in response.headers: + response.headers[OBSERVABILITY_ID_HEADER] = observability_id if runtime_settings.revision: response.headers["X-PolicyEngine-Simulation-Revision"] = ( runtime_settings.revision ) - logger.info( - "simulation_entry_request", - extra={ - "request_id": request_id, - "method": request.method, - "path": _route_template(request), - "status_code": response.status_code, - "elapsed_ms": elapsed_ms, - "backend": "old_gateway", - **_request_identifiers(request), - }, - ) return response async def forward( @@ -238,12 +238,17 @@ async def forward( backend_started = time.monotonic() route = _route_template(request) event_identifiers: RequestIdentifiers = identifiers or {} + try: + runtime.set_context(backend="old_gateway", **event_identifiers) + except Exception: # noqa: BLE001, S110 - telemetry is non-fatal + pass try: result = await runtime_backend.request( method, path, json_body=body, request_id=request.state.request_id, + observability_id=request.state.observability_id, ) attributes: BackendTelemetryAttributes = { "request_id": request.state.request_id, @@ -274,14 +279,19 @@ async def forward( ) if response_identifier_key and response_identifier is not None: attributes[response_identifier_key] = response_identifier - record_event("simulation_entry_backend_response", **attributes) + runtime.event( + "simulation_entry_backend_response", + attributes=attributes, + ) return _response(result) except BackendTimeout: - record_event( + runtime.event( "simulation_entry_backend_timeout", - request_id=request.state.request_id, - route=route, - **event_identifiers, + attributes={ + "request_id": request.state.request_id, + "route": route, + **event_identifiers, + }, ) return JSONResponse( status_code=504, @@ -292,11 +302,13 @@ async def forward( }, ) except BackendUnavailable: - record_event( + runtime.event( "simulation_entry_backend_unavailable", - request_id=request.state.request_id, - route=route, - **event_identifiers, + attributes={ + "request_id": request.state.request_id, + "route": route, + **event_identifiers, + }, ) return JSONResponse( status_code=503, @@ -348,6 +360,7 @@ async def submit_temporary_stage12_report( comparison.submit_temporary_report( request_payload=_model_json(body), request_id=request.state.request_id, + observability_id=request.state.observability_id, ), timeout=STAGE12_MODAL_SUBMISSION_TIMEOUT_SECONDS, ) @@ -449,10 +462,17 @@ async def get_temporary_stage12_report( ComparisonRunLifecycleStatus.PENDING, ComparisonRunLifecycleStatus.RUNNING, } + response_headers = {"Retry-After": "5"} if running else {} + if report.observability_id is not None: + response_headers[OBSERVABILITY_ID_HEADER] = report.observability_id + try: + runtime.set_context(observability_id=report.observability_id) + except Exception: # noqa: BLE001, S110 - telemetry is non-fatal + pass return JSONResponse( status_code=202 if running else 200, content=payload.model_dump(mode="json"), - headers={"Retry-After": "5"} if running else None, + headers=response_headers or None, ) @app.post( @@ -500,6 +520,7 @@ async def submit_comparison( request_payload=request_payload, production_response=bytes(response.body), request_id=request.state.request_id, + observability_id=request.state.observability_id, ), timeout=STAGE12_MODAL_SUBMISSION_TIMEOUT_SECONDS, ) diff --git a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/auth.py b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/auth.py index 2d72d5f24..a3f332b99 100644 --- a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/auth.py +++ b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/auth.py @@ -8,7 +8,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer import jwt -from policyengine_observability import record_event +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_entry.config import Settings from policyengine_simulation_entry.schemas import CallerIdentity @@ -29,9 +29,13 @@ def __init__(self, issuer: str, audience: str): def __call__( self, token: HTTPAuthorizationCredentials | None, + runtime: ObservabilityRuntime, ) -> CallerIdentity: if token is None: - record_event("simulation_entry_auth_rejected", reason="missing_token") + runtime.event( + "simulation_entry_auth_rejected", + attributes={"reason": "missing_token"}, + ) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) try: @@ -53,7 +57,10 @@ def __call__( "invalid_simulation_entry_bearer_token", extra={"error_type": reason}, ) - record_event("simulation_entry_auth_rejected", reason=reason) + runtime.event( + "simulation_entry_auth_rejected", + attributes={"reason": reason}, + ) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) from error @@ -65,8 +72,9 @@ def _decoder(issuer: str, audience: str) -> JWTDecoder: class CallerAuthenticator: """FastAPI dependency that preserves the gateway's JWT contract.""" - def __init__(self, settings: Settings): + def __init__(self, settings: Settings, runtime: ObservabilityRuntime): self.settings = settings + self.runtime = runtime def __call__( self, @@ -77,7 +85,7 @@ def __call__( return _decoder( self.settings.auth_issuer, self.settings.auth_audience, - )(token) + )(token, self.runtime) def reset_decoder_cache() -> None: diff --git a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/backend.py b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/backend.py index 065547535..c08996668 100644 --- a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/backend.py +++ b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/backend.py @@ -9,8 +9,13 @@ import httpx from pydantic import ValidationError -from policyengine_observability import REQUEST_ID_HEADER +from policyengine_observability import ( + REQUEST_ID_HEADER, + ObservabilityRuntime, + instrument_httpx, +) from policyengine_simulation_contract.json_types import JsonObject +from policyengine_simulation_observability.identifiers import OBSERVABILITY_ID_HEADER from policyengine_simulation_entry.config import Settings from policyengine_simulation_entry.schemas import BackendServiceToken @@ -23,6 +28,7 @@ "etag", "retry-after", REQUEST_ID_HEADER.lower(), + OBSERVABILITY_ID_HEADER.lower(), } ) @@ -60,6 +66,7 @@ async def request( *, json_body: JsonObject | None = None, request_id: str | None = None, + observability_id: str | None = None, ) -> BackendResponse: ... @@ -122,9 +129,11 @@ def __init__( settings: Settings, *, transport: httpx.AsyncBaseTransport | None = None, + runtime: ObservabilityRuntime | None = None, ): self.settings = settings self.transport = transport + self.observability = runtime self.client: httpx.AsyncClient | None = None self.token_provider: ClientCredentialsTokenProvider | None = None @@ -142,6 +151,8 @@ async def start(self) -> None: transport=self.transport, follow_redirects=False, ) + if self.observability is not None: + instrument_httpx(self.client, self.observability) self.token_provider = ClientCredentialsTokenProvider( self.settings, self.client, @@ -172,6 +183,7 @@ async def request( *, json_body: JsonObject | None = None, request_id: str | None = None, + observability_id: str | None = None, ) -> BackendResponse: client, token_provider = self._runtime() method = method.upper() @@ -183,6 +195,8 @@ async def request( headers = {"Authorization": f"Bearer {token}"} if request_id: headers[REQUEST_ID_HEADER] = request_id + if observability_id: + headers[OBSERVABILITY_ID_HEADER] = observability_id try: response = await client.request( diff --git a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/stage12_backend.py b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/stage12_backend.py index 1a3a9934a..e5615a63a 100644 --- a/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/stage12_backend.py +++ b/projects/policyengine-simulation-entry/src/policyengine_simulation_entry/stage12_backend.py @@ -10,7 +10,7 @@ from uuid import NAMESPACE_URL, UUID, uuid4, uuid5 import modal -from policyengine_observability import record_event +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_contract.stage12_execution import ( ComparisonReportRecord, ComparisonRunAggregationStatus, @@ -25,6 +25,14 @@ V2WorkerVersion, ) from policyengine_stage12_persistence import Stage12PersistenceStore +from policyengine_simulation_observability.identifiers import ( + normalize_observability_id, +) +from policyengine_simulation_observability.stages import ( + STAGE12_CANONICAL_REPORT_STAGES, + STAGE12_SHADOW_REPORT_STAGES, + Stage, +) from policyengine_simulation_entry.config import Settings from policyengine_simulation_entry.stage12_adapter import adapt_annual_comparison @@ -55,6 +63,7 @@ async def spawn( report_payload: dict[str, Any], context_payload: dict[str, Any], parent_payload: dict[str, Any], + observability_context: dict[str, Any] | None = None, ) -> str: ... @@ -95,6 +104,7 @@ async def spawn( report_payload: dict[str, Any], context_payload: dict[str, Any], parent_payload: dict[str, Any], + observability_context: dict[str, Any] | None = None, ) -> str: function = modal.Function.from_name( worker.application_name, @@ -107,6 +117,7 @@ async def spawn( report_payload, context_payload, parent_payload, + observability_context, ) invocation_id = getattr(call, "object_id", None) if not isinstance(invocation_id, str) or not invocation_id: @@ -124,14 +135,21 @@ def __init__( manifest_loader: V2ManifestLoader, store: ComparisonStore, invoker: ReportInvoker, + runtime: ObservabilityRuntime, ) -> None: self._settings = settings self._manifest_loader = manifest_loader self._store = store self._invoker = invoker + self._runtime = runtime @classmethod - def from_settings(cls, settings: Settings) -> Stage12ComparisonBackend: + def from_settings( + cls, + settings: Settings, + *, + runtime: ObservabilityRuntime, + ) -> Stage12ComparisonBackend: manifest_store = modal.Dict.from_name( settings.stage12_v2_manifest_name, environment_name=settings.stage12_v2_manifest_environment, @@ -142,6 +160,7 @@ def from_settings(cls, settings: Settings) -> Stage12ComparisonBackend: manifest_loader=V2ManifestLoader(manifest_store), store=Stage12PersistenceStore(settings.stage12_database_url), invoker=ModalReportInvoker(settings.stage12_v2_manifest_environment), + runtime=runtime, ) async def dispatch_after_production( @@ -150,6 +169,7 @@ async def dispatch_after_production( request_payload: dict[str, Any], production_response: bytes, request_id: str, + observability_id: str, ) -> None: prepared = await asyncio.to_thread( self._prepare_automatic_report, @@ -169,6 +189,7 @@ async def dispatch_after_production( manifest_sha256=manifest_sha256, request_id=request_id, production_function_call_id=parent.production_identity, + observability_id=observability_id, ) # TEMPORARY(Stage 12): Remove this operator submission seam when Stage 14 @@ -178,6 +199,7 @@ async def submit_temporary_report( *, request_payload: dict[str, Any], request_id: str, + observability_id: str, ) -> ComparisonReportRecord: """Submit one direct report and return after Modal acknowledges it.""" @@ -196,6 +218,7 @@ async def submit_temporary_report( manifest_sha256=manifest_sha256, request_id=request_id, production_function_call_id=None, + observability_id=observability_id, ) async def get_temporary_report( @@ -360,15 +383,17 @@ def _prepare_automatic_report( worker=worker, ) if adapted.report is None: - record_event( + self._runtime.event( "stage12_comparison_run_skipped", - request_id=request_id, - production_identity=production_identity, - reason=( - adapted.skip_reason.value - if adapted.skip_reason - else "unsupported_input" - ), + attributes={ + "request_id": request_id, + "production_identity": production_identity, + "reason": ( + adapted.skip_reason.value + if adapted.skip_reason + else "unsupported_input" + ), + }, ) return None country = request_payload.get("country") @@ -410,6 +435,7 @@ async def _spawn_report( manifest_sha256: str, request_id: str, production_function_call_id: str | None, + observability_id: str, ) -> ComparisonReportRecord: """Return after Modal accepts the coordinator; perform no database writes.""" @@ -419,27 +445,61 @@ async def _spawn_report( f"{parent.evaluation_id}" ) try: - invocation_id = await self._invoker.spawn( - worker=worker, - report_payload=report.model_dump(mode="json"), - parent_payload=parent.model_dump(mode="json"), - context_payload={ - "request_id": request_id, - "environment": self._settings.environment, - "modal_environment": ( - self._settings.stage12_v2_manifest_environment + raw_context = self._runtime.capture_context() + except Exception: + raw_context = None + captured_context = dict(raw_context) if isinstance(raw_context, dict) else {} + captured_context.pop("observability_id", None) + resolved_observability_id = normalize_observability_id(observability_id) + if resolved_observability_id is not None: + captured_context["observability_id"] = resolved_observability_id + parent = parent.model_copy( + update={"observability_id": resolved_observability_id} + ) + stage_plan = ( + STAGE12_SHADOW_REPORT_STAGES + if production_function_call_id is not None + else STAGE12_CANONICAL_REPORT_STAGES + ) + try: + with self._runtime.span( + stage_plan.name(Stage.STAGE12_ENTRY_DISPATCH), + attributes={ + "evaluation_id": str(parent.evaluation_id), + "execution_mode": ( + "shadow" + if production_function_call_id is not None + else "authoritative" ), - "worker_version": version, - "modal_application": worker.application_name, - "simulation_callable": country_worker.single_simulation_callable, - "version_manifest_sha256": manifest_sha256, - "bundle_manifest_sha256": worker.bundle_manifest_sha256, - "artifact_prefix": artifact_prefix, - "production_function_call_id": production_function_call_id, - "created_at": parent.created_at.isoformat(), - "retention_expires_at": parent.retention_expires_at.isoformat(), + "runner_name": "stage12", }, - ) + ): + invocation_id = await self._invoker.spawn( + worker=worker, + report_payload=report.model_dump(mode="json"), + parent_payload=parent.model_dump(mode="json"), + observability_context=captured_context or None, + context_payload={ + "request_id": request_id, + "environment": self._settings.environment, + "modal_environment": ( + self._settings.stage12_v2_manifest_environment + ), + "worker_version": version, + "modal_application": worker.application_name, + "simulation_callable": ( + country_worker.single_simulation_callable + ), + "version_manifest_sha256": manifest_sha256, + "bundle_manifest_sha256": worker.bundle_manifest_sha256, + "artifact_prefix": artifact_prefix, + "production_function_call_id": production_function_call_id, + "created_at": parent.created_at.isoformat(), + "retention_expires_at": ( + parent.retention_expires_at.isoformat() + ), + }, + ) except Exception as error: # noqa: BLE001 failed_at = datetime.now(UTC) failed = parent.model_copy( @@ -462,13 +522,15 @@ async def _spawn_report( "updated_at": acknowledged_at, } ) - record_event( + self._runtime.event( "stage12_comparison_run_dispatched", - request_id=request_id, - evaluation_id=str(parent.evaluation_id), - production_identity=parent.production_identity, - worker_version=version, - modal_application=worker.application_name, - coordinator_invocation_id=invocation_id, + attributes={ + "request_id": request_id, + "evaluation_id": str(parent.evaluation_id), + "production_identity": parent.production_identity, + "worker_version": version, + "modal_application": worker.application_name, + "coordinator_invocation_id": invocation_id, + }, ) return acknowledged diff --git a/projects/policyengine-simulation-entry/tests/conftest.py b/projects/policyengine-simulation-entry/tests/conftest.py index 53cc19b70..524b7142b 100644 --- a/projects/policyengine-simulation-entry/tests/conftest.py +++ b/projects/policyengine-simulation-entry/tests/conftest.py @@ -38,6 +38,7 @@ class BackendRequest: path: str json_body: JsonObject | None request_id: str | None + observability_id: str | None class FakeBackend: @@ -63,6 +64,7 @@ async def request( *, json_body: JsonObject | None = None, request_id: str | None = None, + observability_id: str | None = None, ) -> BackendResponse: self.requests.append( BackendRequest( @@ -70,6 +72,7 @@ async def request( path=path, json_body=json_body, request_id=request_id, + observability_id=observability_id, ) ) return self.responses.get( diff --git a/projects/policyengine-simulation-entry/tests/test_app.py b/projects/policyengine-simulation-entry/tests/test_app.py index 2a36a7b0f..a3fd0b25c 100644 --- a/projects/policyengine-simulation-entry/tests/test_app.py +++ b/projects/policyengine-simulation-entry/tests/test_app.py @@ -3,23 +3,17 @@ import asyncio import json import logging +from unittest.mock import Mock import pytest from conftest import FakeBackend, make_settings from fastapi import HTTPException -from policyengine_observability import REQUEST_ID_HEADER, current_context +from policyengine_observability import REQUEST_ID_HEADER from policyengine_simulation_contract.json_types import JsonObject from policyengine_simulation_contract.stage12_execution import ( ComparisonRunLifecycleStatus, SimulationRole, ) -from stage12_fixtures import ( - EVALUATION_ID, - comparison_report, - comparison_simulation, - eligible_payload, -) - from policyengine_simulation_entry import app as app_module from policyengine_simulation_entry.app import create_app from policyengine_simulation_entry.backend import ( @@ -31,13 +25,29 @@ TemporaryStage12DispatchFailed, TemporaryStage12UnsupportedRequest, ) +from policyengine_simulation_observability.identifiers import OBSERVABILITY_ID_HEADER +from stage12_fixtures import ( + EVALUATION_ID, + comparison_report, + comparison_simulation, + eligible_payload, +) -def response(status: int, payload: JsonObject) -> BackendResponse: +def response( + status: int, + payload: JsonObject, + *, + headers: dict[str, str] | None = None, +) -> BackendResponse: return BackendResponse( status_code=status, content=json.dumps(payload).encode(), - headers={"content-type": "application/json", "retry-after": "3"}, + headers={ + "content-type": "application/json", + "retry-after": "3", + **(headers or {}), + }, ) @@ -152,6 +162,7 @@ async def dispatch_after_production(self, **call): from fastapi.testclient import TestClient + observability_id = "00000000-0000-4000-8000-000000000001" with TestClient(app) as test_client: result = test_client.post( "/simulate/economy/comparison", @@ -160,12 +171,14 @@ async def dispatch_after_production(self, **call): "scope": "macro", "reform": {}, "_telemetry": { - "run_id": "production-run-1", - "process_id": "api-process-1", + "submission_claim_id": "api-process-1", "capture_mode": "disabled", }, }, - headers={REQUEST_ID_HEADER: "request-1"}, + headers={ + REQUEST_ID_HEADER: "request-1", + OBSERVABILITY_ID_HEADER: observability_id, + }, ) assert result.status_code == 202 @@ -173,9 +186,8 @@ async def dispatch_after_production(self, **call): assert result.headers["x-policyengine-simulation-backend"] == "old_gateway" assert len(comparison.calls) == 1 assert comparison.calls[0]["request_id"] == "request-1" - assert comparison.calls[0]["request_payload"]["telemetry"]["run_id"] == ( - "production-run-1" - ) + assert comparison.calls[0]["observability_id"] == observability_id + assert "observability_id" not in comparison.calls[0]["request_payload"]["telemetry"] assert json.loads(comparison.calls[0]["production_response"]) == payload @@ -398,7 +410,11 @@ def test_temporary_stage12_poll_reads_durable_parent_and_children(backend): def test_temporary_stage12_poll_returns_terminal_metadata_with_200(backend): comparison = TemporaryComparisonBackend( - report=comparison_report(status=ComparisonRunLifecycleStatus.SUCCEEDED) + report=comparison_report( + status=ComparisonRunLifecycleStatus.SUCCEEDED + ).model_copy( + update={"observability_id": "00000000-0000-4000-8000-000000000012"} + ) ) app = create_app( settings=make_settings(), @@ -414,6 +430,14 @@ def test_temporary_stage12_poll_returns_terminal_metadata_with_200(backend): assert result.status_code == 200 assert "retry-after" not in result.headers + assert ( + result.headers[OBSERVABILITY_ID_HEADER] + == "00000000-0000-4000-8000-000000000012" + ) + assert ( + result.json()["report"]["observability_id"] + == "00000000-0000-4000-8000-000000000012" + ) assert result.json()["report"]["aggregate_output_uri"] == ( "gs://stage12-private/report.json" ) @@ -554,15 +578,18 @@ async def submit_temporary_report(self, **_): def test_job_status_preserves_id_and_status(client, backend): + observability_id = "00000000-0000-4000-8000-000000000001" backend.responses[("GET", "/jobs/fc-123")] = response( 202, - {"status": "running", "run_id": "run-1"}, + {"status": "running"}, + headers={OBSERVABILITY_ID_HEADER: observability_id}, ) result = client.get("/jobs/fc-123") assert result.status_code == 202 - assert result.json() == {"status": "running", "run_id": "run-1"} + assert result.json() == {"status": "running"} + assert result.headers[OBSERVABILITY_ID_HEADER] == observability_id assert backend.requests[-1].path == "/jobs/fc-123" @@ -633,12 +660,9 @@ def test_job_status_records_structured_backend_telemetry( backend, monkeypatch, ): - events = [] - monkeypatch.setattr( - app_module, - "record_event", - lambda name, **attributes: events.append((name, attributes)), - ) + runtime = client.app.state.policyengine_observability + event = Mock(wraps=runtime.event) + monkeypatch.setattr(runtime, "event", event) backend.responses[("GET", "/jobs/fc-123")] = response( 202, {"status": "running", "job_id": "must-not-be-recorded"}, @@ -646,7 +670,13 @@ def test_job_status_records_structured_backend_telemetry( client.get("/jobs/fc-123") - name, attributes = events[-1] + call = next( + item + for item in event.call_args_list + if item.args == ("simulation_entry_backend_response",) + ) + name = call.args[0] + attributes = call.kwargs["attributes"] assert name == "simulation_entry_backend_response" assert attributes["job_state"] == "running" assert attributes["status_code"] == 202 @@ -654,33 +684,6 @@ def test_job_status_records_structured_backend_telemetry( assert attributes["job_id"] == "fc-123" -def test_request_log_templates_route_and_keeps_structured_job_id( - client, - backend, - caplog, -): - backend.responses[("GET", "/jobs/fc-123")] = BackendResponse( - 202, - b'{"job_id":"fc-123","status":"running"}', - {"content-type": "application/json"}, - ) - - with caplog.at_level(logging.INFO): - result = client.get( - "/jobs/fc-123", - headers={"Authorization": "Bearer caller"}, - ) - - assert result.status_code == 202 - request_record = next( - record - for record in caplog.records - if record.getMessage() == "simulation_entry_request" - ) - assert request_record.path == "/jobs/{job_id}" - assert request_record.job_id == "fc-123" - - def test_budget_window_routes_use_original_batch_id(client, backend): backend.responses[("POST", "/simulate/economy/budget-window")] = response( 202, @@ -728,53 +731,46 @@ def test_versions_and_ping_are_public_proxy_routes(client, backend): def test_request_id_is_propagated_logged_and_returned( client, backend, - caplog, ): - with caplog.at_level(logging.INFO): - result = client.get( - "/versions", - headers={REQUEST_ID_HEADER: "request-123"}, - ) + result = client.get( + "/versions", + headers={REQUEST_ID_HEADER: "request-123"}, + ) assert result.headers[REQUEST_ID_HEADER] == "request-123" assert "x-request-id" not in result.headers assert backend.requests[-1].request_id == "request-123" - request_record = next( - record - for record in caplog.records - if record.getMessage() == "simulation_entry_request" - ) - assert request_record.request_id == "request-123" -def test_generated_request_id_is_shared_with_observability(): - class CapturingBackend(FakeBackend): - observability_request_id: str | None = None - - async def request(self, *args, **kwargs): - context = current_context() - self.observability_request_id = ( - context.request_id if context is not None else None - ) - return await super().request(*args, **kwargs) - - backend = CapturingBackend() +def test_request_identifiers_are_attached_to_the_active_observability_context( + backend, +): app = create_app( settings=make_settings(), backend=backend, auth_dependency=lambda: None, ) + runtime = app.state.policyengine_observability + + @app.get("/_test/runtime-context") + def runtime_context(): + return runtime.capture_context() from fastapi.testclient import TestClient - with TestClient(app) as client: - result = client.get("/versions") + observability_id = "00000000-0000-4000-8000-000000000001" + with TestClient(app) as test_client: + result = test_client.get( + "/_test/runtime-context", + headers={ + REQUEST_ID_HEADER: "request-123", + OBSERVABILITY_ID_HEADER: observability_id, + }, + ) - request_id = result.headers[REQUEST_ID_HEADER] - assert request_id - assert "x-request-id" not in result.headers - assert backend.requests[-1].request_id == request_id - assert backend.observability_request_id == request_id + assert result.status_code == 200 + assert result.json()["request_id"] == "request-123" + assert result.json()["observability_id"] == observability_id def test_x_request_id_is_not_an_alias(client, backend): @@ -798,7 +794,7 @@ def test_entrypoint_uses_its_own_service_name(backend): ) assert ( - app.state.policyengine_observability.config.service_name + app.state.policyengine_observability.config.service.name == "policyengine-simulation-entry" ) @@ -876,9 +872,8 @@ async def request(self, *args, **kwargs): request_record = next( record for record in caplog.records - if record.getMessage() == "simulation_entry_request" + if record.getMessage() == "simulation_entry_unhandled_request" ) - assert request_record.status_code == 500 assert request_record.path == "/jobs/{job_id}" assert request_record.job_id == "job-1" diff --git a/projects/policyengine-simulation-entry/tests/test_auth.py b/projects/policyengine-simulation-entry/tests/test_auth.py index 22f8edfed..1a1a552b9 100644 --- a/projects/policyengine-simulation-entry/tests/test_auth.py +++ b/projects/policyengine-simulation-entry/tests/test_auth.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone from types import SimpleNamespace +from unittest.mock import Mock import jwt import pytest @@ -48,7 +49,7 @@ def _use_static_signing_key(monkeypatch, public_key) -> None: def test_missing_token_preserves_403_contract(monkeypatch): class Decoder: - def __call__(self, token): + def __call__(self, token, runtime): from fastapi import HTTPException raise HTTPException(status_code=403) @@ -64,7 +65,7 @@ def __call__(self, token): def test_public_routes_do_not_require_token(monkeypatch): class Decoder: - def __call__(self, token): + def __call__(self, token, runtime): raise AssertionError("public route attempted authentication") monkeypatch.setattr(auth_module, "_decoder", lambda issuer, audience: Decoder()) @@ -108,7 +109,8 @@ def test_decoder_returns_only_the_declared_caller_identity(signing_keys): scope="simulate:read", undocumented_claim="ignored", ), - ) + ), + Mock(), ) assert isinstance(identity, CallerIdentity) diff --git a/projects/policyengine-simulation-entry/tests/test_deployment_assets.py b/projects/policyengine-simulation-entry/tests/test_deployment_assets.py index c448e1a42..31a621330 100644 --- a/projects/policyengine-simulation-entry/tests/test_deployment_assets.py +++ b/projects/policyengine-simulation-entry/tests/test_deployment_assets.py @@ -263,6 +263,10 @@ def test_cloud_run_deployment_escapes_environment_and_pins_secret_versions(tmp_p "OLD_GATEWAY_AUTH_AUDIENCE_VALUE": "legacy-api", "OLD_GATEWAY_AUTH_CLIENT_ID_VALUE": "entrypoint-client", "OLD_GATEWAY_AUTH_CLIENT_SECRET_SECRET_NAME": "old-client-secret", + "OBSERVABILITY_SERVICE_NAMESPACE": "policyengine.simulation-entry", + "OBSERVABILITY_TRACE_PROJECT_ID": "policyengine-observability", + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://collector.example", + "POLICYENGINE_OTEL_GOOGLE_AUDIENCE": "https://collector.example", "STAGE12_ENABLED_VALUE": "0", "STAGE12_ARTIFACT_BUCKET_VALUE": "policyengine-stage12-staging", "STAGE12_DATABASE_URL_SECRET_NAME": "stage12-staging-database-url", @@ -282,6 +286,18 @@ def test_cloud_run_deployment_escapes_environment_and_pins_secret_versions(tmp_p ) assert runtime_environment["STAGE12_ENABLED"] == "0" assert runtime_environment["STAGE12_V2_MANIFEST_ENVIRONMENT"] == "staging" + assert runtime_environment["OBSERVABILITY_SERVICE_NAMESPACE"] == ( + "policyengine.simulation-entry" + ) + assert runtime_environment["OBSERVABILITY_TRACE_PROJECT_ID"] == ( + "policyengine-observability" + ) + assert runtime_environment["OTEL_EXPORTER_OTLP_ENDPOINT"] == ( + "https://collector.example" + ) + assert runtime_environment["POLICYENGINE_OTEL_GOOGLE_AUDIENCE"] == ( + "https://collector.example" + ) assert "STAGE12_DATABASE_URL" not in runtime_environment arguments = arguments_capture.read_text(encoding="utf-8").splitlines() secrets_argument = arguments[arguments.index("--set-secrets") + 1] @@ -355,6 +371,12 @@ def test_full_stack_promotion_order_is_explicit(): "needs: [prepare, deploy_entrypoint, deploy_gateway, deploy_executor]" ) assert routing_dependencies in reusable_workflow + gateway_job = reusable_workflow[ + reusable_workflow.index("\n deploy_gateway:") : reusable_workflow.index( + "\n deploy_executor:" + ) + ] + assert "needs: [prepare, deploy_executor]" in gateway_job assert reusable_workflow.index("\n update_routing:") < reusable_workflow.index( "\n integration:" ) diff --git a/projects/policyengine-simulation-entry/tests/test_stage12_adapter.py b/projects/policyengine-simulation-entry/tests/test_stage12_adapter.py index 891f8bb80..ba25314a4 100644 --- a/projects/policyengine-simulation-entry/tests/test_stage12_adapter.py +++ b/projects/policyengine-simulation-entry/tests/test_stage12_adapter.py @@ -76,8 +76,7 @@ def test_absent_package_versions_resolve_from_the_selected_bundle() -> None: def test_normalized_production_telemetry_does_not_change_eligibility() -> None: payload = eligible_payload() payload["telemetry"] = { - "run_id": "production-run-1", - "process_id": "api-process-1", + "submission_claim_id": "api-process-1", "capture_mode": "disabled", } diff --git a/projects/policyengine-simulation-entry/tests/test_stage12_backend.py b/projects/policyengine-simulation-entry/tests/test_stage12_backend.py index c465bb47d..9c3b8a4cc 100644 --- a/projects/policyengine-simulation-entry/tests/test_stage12_backend.py +++ b/projects/policyengine-simulation-entry/tests/test_stage12_backend.py @@ -5,6 +5,7 @@ import asyncio import json from types import SimpleNamespace +from unittest.mock import MagicMock import pytest from conftest import make_settings @@ -22,6 +23,8 @@ TemporaryStage12UnsupportedRequest, ) +OBSERVABILITY_ID = "00000000-0000-4000-8000-000000000012" + class FakeLoader: def __init__(self): @@ -63,6 +66,11 @@ async def spawn(self, **call): def _backend(store, invoker, *, environment="staging", modal_environment="staging"): + runtime = MagicMock() + runtime.capture_context.return_value = { + "observability_id": "00000000-0000-4000-8000-000000000099", + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + } return Stage12ComparisonBackend( make_settings( environment=environment, @@ -71,6 +79,7 @@ def _backend(store, invoker, *, environment="staging", modal_environment="stagin manifest_loader=FakeLoader(), store=store, invoker=invoker, + runtime=runtime, ) @@ -103,6 +112,7 @@ def from_name(*args, **kwargs): report_payload={"report": "payload"}, context_payload={"context": "payload"}, parent_payload={"parent": "payload"}, + observability_context={"traceparent": "trace"}, ) assert invocation_id == "modal-call-1" @@ -111,6 +121,7 @@ def from_name(*args, **kwargs): {"report": "payload"}, {"context": "payload"}, {"parent": "payload"}, + {"traceparent": "trace"}, ) ] @@ -124,6 +135,7 @@ def test_automatic_submission_only_awaits_modal_acknowledgement() -> None: request_payload=eligible_payload(), production_response=_response(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) @@ -153,6 +165,7 @@ def test_repeated_automatic_submission_uses_one_deterministic_report_identity() request_payload=eligible_payload(), production_response=_response(), request_id=request_id, + observability_id=OBSERVABILITY_ID, ) ) @@ -176,6 +189,7 @@ def test_unsupported_automatic_input_does_not_dispatch_or_write() -> None: request_payload={**eligible_payload(), "include_cliffs": True}, production_response=_response(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) @@ -196,6 +210,7 @@ def test_dispatch_failure_is_sanitized_without_database_cleanup() -> None: request_payload=eligible_payload(), production_response=_response(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) @@ -220,6 +235,7 @@ async def spawn(self, **call): request_payload=eligible_payload(), production_response=_response(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) await asyncio.sleep(0) @@ -238,11 +254,13 @@ def test_temporary_submission_spawns_without_database_writes() -> None: _backend(store, invoker).submit_temporary_report( request_payload=eligible_payload(), request_id="manual-request-1", + observability_id=OBSERVABILITY_ID, ) ) assert store.events == [] assert report.status is ComparisonRunLifecycleStatus.RUNNING + assert report.observability_id == OBSERVABILITY_ID assert report.production_identity == f"direct:{report.evaluation_id}" assert report.incumbent_execution_id is None assert report.coordinator_invocation_id == "modal-call-1" @@ -253,7 +271,9 @@ def test_temporary_submission_spawns_without_database_writes() -> None: assert call["context_payload"]["production_function_call_id"] is None submitted_parent = ComparisonReportRecord.model_validate(call["parent_payload"]) assert submitted_parent.status is ComparisonRunLifecycleStatus.PENDING + assert submitted_parent.observability_id == report.observability_id assert submitted_parent.coordinator_invocation_id is None + assert call["observability_context"]["observability_id"] == report.observability_id def test_production_context_keeps_logical_and_modal_environments_distinct() -> None: @@ -269,6 +289,7 @@ def test_production_context_keeps_logical_and_modal_environments_distinct() -> N backend.submit_temporary_report( request_payload=eligible_payload(), request_id="production-manual-request", + observability_id=OBSERVABILITY_ID, ) ) @@ -285,12 +306,14 @@ def test_each_temporary_submission_creates_a_distinct_report_identity() -> None: backend.submit_temporary_report( request_payload=eligible_payload(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) second = asyncio.run( backend.submit_temporary_report( request_payload=eligible_payload(), request_id="request-2", + observability_id=OBSERVABILITY_ID, ) ) @@ -307,6 +330,7 @@ def test_temporary_submission_rejects_unsupported_input_without_side_effects() - _backend(store, invoker).submit_temporary_report( request_payload={**eligible_payload(), "include_cliffs": True}, request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) @@ -322,6 +346,7 @@ def test_temporary_status_reads_coordinator_owned_postgres_state() -> None: _backend(store, invoker).submit_temporary_report( request_payload=eligible_payload(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) store.current = report @@ -344,6 +369,7 @@ def test_temporary_status_rejects_a_record_from_another_environment() -> None: staging_backend.submit_temporary_report( request_payload=eligible_payload(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) ) store = FakeStore(current=report) @@ -373,6 +399,7 @@ async def fake_to_thread(function, *args, **kwargs): report = await backend.submit_temporary_report( request_payload=eligible_payload(), request_id="request-1", + observability_id=OBSERVABILITY_ID, ) assert calls == [ diff --git a/projects/policyengine-simulation-entry/uv.lock b/projects/policyengine-simulation-entry/uv.lock index bd4a6636b..40eed03f1 100644 --- a/projects/policyengine-simulation-entry/uv.lock +++ b/projects/policyengine-simulation-entry/uv.lock @@ -93,15 +93,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] -[[package]] -name = "asgiref" -version = "3.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -265,27 +256,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - [[package]] name = "fastapi" version = "0.115.14" @@ -341,6 +311,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-api-core" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/e5/18aeff14213db86267a0f79d869352954394ab4ab3a3866d9b9b0e82dc6a/google_api_core-2.38.0.tar.gz", hash = "sha256:31e326eafa31b34f1db7a715f50f61dbca7e6e37277762f252ed90e6ce94c246", size = 206784, upload-time = "2026-09-17T20:30:08.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/55/e545e4b0040cb812f65d2abc545c6420f57b687d2af130a447c86d14d00b/google_api_core-2.38.0-py3-none-any.whl", hash = "sha256:8db2730375eba434bd75041fbe06b5374287407689ce132ac5650c8c8f042fcc", size = 187996, upload-time = "2026-09-17T20:29:32.813Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, +] + +[[package]] +name = "google-cloud-appengine-logging" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, +] + +[[package]] +name = "google-cloud-logging" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -353,6 +422,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "greenlet" version = "3.5.6" @@ -369,6 +443,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, ] +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, +] + [[package]] name = "grpcio" version = "1.82.1" @@ -390,6 +478,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/42/66a98d47732e35290bef722f6149fed3709cd4cf61166f6f53a12f417302/grpcio-1.82.1-cp313-cp313-win_amd64.whl", hash = "sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90", size = 5000980, upload-time = "2026-07-08T12:35:46.262Z" }, ] +[[package]] +name = "grpcio-status" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/26/0aa9168c87882381fd810d140c279a2490ed6aee655f0515d6f56c5ca404/grpcio_status-1.81.1.tar.gz", hash = "sha256:9389a03e746017b10f0630c064289201458f3ce01f5d7ef4b0bebc1ef6cf82ad", size = 13923, upload-time = "2026-06-11T12:58:48.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl", hash = "sha256:08072fa9995f4a95c647fc6f4f85e2411573d00087bcabdf30f260114338f232", size = 14638, upload-time = "2026-06-11T12:58:31.982Z" }, +] + [[package]] name = "grpclib" version = "0.4.9" @@ -480,18 +582,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -513,24 +603,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "logfire" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/fa/14cbd976e8ff77290c0c87f93c45029785537ffb84e2f5215416786cda11/logfire-4.6.0.tar.gz", hash = "sha256:974bc8f4efd3aa9df2d0c7b1d53b35dc4612ff5cee2be6c40dd65e5c26eab694", size = 533174, upload-time = "2025-09-10T14:56:42.168Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/9c/6384dc69601bfba19c1f5909a472b00cc4b5863f411ecf85be308beacf8f/logfire-4.6.0-py3-none-any.whl", hash = "sha256:d322844db7a2f1935976f2852a07e3753489ecc4e83fa288173b05a453bb1cbf", size = 219556, upload-time = "2025-09-10T14:56:37.278Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -669,174 +741,83 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703, upload-time = "2025-02-04T18:17:13.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955, upload-time = "2025-02-04T18:16:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d7/44098bf1ef89fc5810cdbda05faa2ae9322a0dbda4921cdc965dc68a9856/opentelemetry_exporter_otlp_proto_common-1.30.0.tar.gz", hash = "sha256:ddbfbf797e518411857d0ca062c957080279320d6235a279f7b64ced73c13897", size = 19640, upload-time = "2025-02-04T18:17:16.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/54/f4b3de49f8d7d3a78fd6e6e1a6fd27dd342eb4d82c088b9078c6a32c3808/opentelemetry_exporter_otlp_proto_common-1.30.0-py3-none-any.whl", hash = "sha256:5468007c81aa9c44dc961ab2cf368a29d3475977df83b4e30aeed42aa7bc3b38", size = 18747, upload-time = "2025-02-04T18:16:51.512Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/3e/c7246df92c25e6ce95c349ad21597b4471b01ec9471e95d5261f1629fe92/opentelemetry_exporter_otlp_proto_grpc-1.30.0.tar.gz", hash = "sha256:d0f10f0b9b9a383b7d04a144d01cb280e70362cccc613987e234183fd1f01177", size = 26256, upload-time = "2025-02-04T18:17:16.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/35/d9f63fd84c2ed8dbd407bcbb933db4ed6e1b08e7fbdaca080b9ac309b927/opentelemetry_exporter_otlp_proto_grpc-1.30.0-py3-none-any.whl", hash = "sha256:2906bcae3d80acc54fd1ffcb9e44d324e8631058b502ebe4643ca71d1ff30830", size = 18550, upload-time = "2025-02-04T18:16:52.532Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/f9/abb9191d536e6a2e2b7903f8053bf859a76bf784e3ca19a5749550ef19e4/opentelemetry_exporter_otlp_proto_http-1.30.0.tar.gz", hash = "sha256:c3ae75d4181b1e34a60662a6814d0b94dd33b628bee5588a878bed92cee6abdc", size = 15073, upload-time = "2025-02-04T18:17:18.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/3c/cdf34bc459613f2275aff9b258f35acdc4c4938dad161d17437de5d4c034/opentelemetry_exporter_otlp_proto_http-1.30.0-py3-none-any.whl", hash = "sha256:9578e790e579931c5ffd50f1e6975cbdefb6a0a0a5dea127a6ae87df10e0a589", size = 17245, upload-time = "2025-02-04T18:16:53.514Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5a/4c7f02235ac1269b48f3855f6be1afc641f31d4888d28b90b732fbce7141/opentelemetry_instrumentation-0.51b0.tar.gz", hash = "sha256:4ca266875e02f3988536982467f7ef8c32a38b8895490ddce9ad9604649424fa", size = 27760, upload-time = "2025-02-04T18:21:09.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/2c/48fa93f1acca9f79a06da0df7bfe916632ecc7fce1971067b3e46bcae55b/opentelemetry_instrumentation-0.51b0-py3-none-any.whl", hash = "sha256:c6de8bd26b75ec8b0e54dff59e198946e29de6a10ec65488c357d4b34aa5bdcf", size = 30923, upload-time = "2025-02-04T18:19:37.829Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/67/8aa6e1129f641f0f3f8786e6c5d18c1f2bbe490bd4b0e91a6879e85154d2/opentelemetry_instrumentation_asgi-0.51b0.tar.gz", hash = "sha256:b3fe97c00f0bfa934371a69674981d76591c68d937b6422a5716ca21081b4148", size = 24201, upload-time = "2025-02-04T18:21:14.321Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/0a95ab37302729543631a789ba8e71dea75c520495739dbbbdfdc580b401/opentelemetry_instrumentation_asgi-0.51b0-py3-none-any.whl", hash = "sha256:e8072993db47303b633c6ec1bc74726ba4d32bd0c46c28dfadf99f79521a324c", size = 16340, upload-time = "2025-02-04T18:19:49.924Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/8db4422b5084177d1ef6c7855c69bf2e9e689f595a4a9b59e60588e0d427/opentelemetry_instrumentation_fastapi-0.51b0.tar.gz", hash = "sha256:1624e70f2f4d12ceb792d8a0c331244cd6723190ccee01336273b4559bc13abc", size = 19249, upload-time = "2025-02-04T18:21:28.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117, upload-time = "2025-02-04T18:20:15.267Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/d5/4a3990c461ae7e55212115e0f8f3aa412b5ce6493579e85c292245ac69ea/opentelemetry_instrumentation_httpx-0.51b0.tar.gz", hash = "sha256:061d426a04bf5215a859fea46662e5074f920e5cbde7e6ad6825a0a1b595802c", size = 17700, upload-time = "2025-02-04T18:21:31.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ba/23d4ab6402408c01f1c3f32e0c04ea6dae575bf19bcb9a0049c9e768c983/opentelemetry_instrumentation_httpx-0.51b0-py3-none-any.whl", hash = "sha256:2e3fdf755ba6ead6ab43031497c3d55d4c796d0368eccc0ce48d304b7ec6486a", size = 14109, upload-time = "2025-02-04T18:20:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/6e/c1ff2e3b0cd3a189a6be03fd4d63441d73d7addd9117ab5454e667b9b6c7/opentelemetry_proto-1.30.0.tar.gz", hash = "sha256:afe5c9c15e8b68d7c469596e5b32e8fc085eb9febdd6fb4e20924a93a0389179", size = 34362, upload-time = "2025-02-04T18:17:28.099Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/d7/85de6501f7216995295f7ec11e470142e6a6e080baacec1753bbf272e007/opentelemetry_proto-1.30.0-py3-none-any.whl", hash = "sha256:c6290958ff3ddacc826ca5abbeb377a31c2334387352a259ba0df37c243adc11", size = 55854, upload-time = "2025-02-04T18:17:08.024Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633, upload-time = "2025-02-04T18:17:28.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717, upload-time = "2025-02-04T18:17:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191, upload-time = "2025-02-04T18:17:29.903Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416, upload-time = "2025-02-04T18:17:11.305Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/64/32510c0a803465eb6ef1f5bd514d0f5627f8abc9444ed94f7240faf6fcaa/opentelemetry_util_http-0.51b0.tar.gz", hash = "sha256:05edd19ca1cc3be3968b1e502fd94816901a365adbeaab6b6ddb974384d3a0b9", size = 8043, upload-time = "2025-02-04T18:21:59.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/dd/c371eeb9cc78abbdad231a27ce1a196a37ef96328d876ccbb381dea4c8ee/opentelemetry_util_http-0.51b0-py3-none-any.whl", hash = "sha256:0561d7a6e9c422b9ef9ae6e77eafcfcd32a2ab689f5e801475cbb67f189efa20", size = 7304, upload-time = "2025-02-04T18:21:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] @@ -859,25 +840,29 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "1.3.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2a/5e0e4c0f30b20a59ffff14364e15374f9eee494f658f41559aba97c152a2/policyengine_observability-1.3.0.tar.gz", hash = "sha256:4c2f3bb2ee59886aef6c90f1edb9c0b957ace75b1518616c0e61905b8be431e0", size = 107961, upload-time = "2026-07-01T21:10:53.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/a2/80e4bc1c4923e8f4c04483bfd22df2500b0d132c4ce1a6dec6749c94d947/policyengine_observability-1.3.0-py3-none-any.whl", hash = "sha256:7a9444e2cb8ddd7f6d54d601bd1e0a7651bd44783b78627b293a27e741f91930", size = 31087, upload-time = "2026-07-01T21:10:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +httpx = [ + { name = "httpx" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-contract" @@ -913,7 +898,7 @@ dependencies = [ { name = "fastapi" }, { name = "httpx" }, { name = "modal" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "httpx", "otlp-grpc"] }, { name = "policyengine-simulation-contract" }, { name = "policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence" }, @@ -939,7 +924,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.0,<1" }, { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "httpx", "otlp-grpc"], specifier = ">=3,<4" }, { name = "policyengine-simulation-contract", editable = "../../libs/policyengine-simulation-contract" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence", editable = "../../libs/policyengine-stage12-persistence" }, @@ -958,9 +943,7 @@ version = "0.1.0" source = { editable = "../../libs/policyengine-simulation-observability" } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -969,9 +952,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1045,18 +1026,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "proto-plus" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, +] + [[package]] name = "protobuf" -version = "5.29.6" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, - { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, - { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -1094,6 +1088,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/7f/e350e1cf498ba2565c3f87b12f429d2012eb86b76c2b3845a19ee5fbb4d6/psycopg_binary-3.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:0ebfad5d131de9f892ae9e70cc7616207768b6714b66a52d4612b8ceaf78b372", size = 3658179, upload-time = "2026-09-18T13:20:22.691Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1462,25 +1477,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - [[package]] name = "yarl" version = "1.24.5" @@ -1511,12 +1507,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, ] - -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -] diff --git a/projects/policyengine-simulation-executor/README.md b/projects/policyengine-simulation-executor/README.md index d1bde8caf..638892281 100644 --- a/projects/policyengine-simulation-executor/README.md +++ b/projects/policyengine-simulation-executor/README.md @@ -111,25 +111,16 @@ Operational notes: ## Observability -The service currently runs two observability backends in parallel: +`policyengine-observability` 3.x emits structured request, operation, +error, and timing logs. The deployment workflow supplies the log project and +name separately from the OpenTelemetry collector endpoint, so this service +does not embed a destination or Google Cloud identity in its source. -- `policyengine-observability` emits structured request, operation, error, - and runtime timing logs. -- Logfire remains enabled as the legacy platform for existing dashboards and - alerting while we evaluate replacing it with another observability platform. - -New instrumentation should be added through `policyengine-observability`; the -Logfire path is retained for continuity during that evaluation. - -For `policyengine-observability`, this service intentionally forces: - -- `log_destinations=("stdout",)` -- `otel_enabled=False` -- `google_cloud_project=None` - -Cloud Logging and OTel export are therefore disabled until the target GCP -project is ready. The package does not currently provide memory-usage -measurements, so memory is not emitted. +Modal writes structured JSON immediately to standard output and also sends it +through a bounded background queue to Cloud Logging. Modal's injected OIDC +identity is exchanged through the dedicated `modal-api-v1` Workload Identity +Federation provider, whose conditions admit only the API v1 gateway and +versioned executor application names in the approved environments. Modal captures container output and exposes it through the app logs UI and CLI. Useful `policyengine-observability` checks after deploying: @@ -141,10 +132,4 @@ modal app logs policyengine-simulation-py --tail 100 --search run_simul modal app dashboard policyengine-simulation-gateway ``` -If using Modal source filters, include both `stdout` and `stderr`. The -observability destination is named `stdout`, but its current Python logging -handler writes through the standard stream handler. - -Logfire continues to use the `policyengine-logfire` Modal secret. Worker -functions and the gateway configure Logfire only when `LOGFIRE_TOKEN` is -present. +If using Modal source filters, include both standard output and standard error. diff --git a/projects/policyengine-simulation-executor/pyproject.toml b/projects/policyengine-simulation-executor/pyproject.toml index 8f11530c4..bdefd0579 100644 --- a/projects/policyengine-simulation-executor/pyproject.toml +++ b/projects/policyengine-simulation-executor/pyproject.toml @@ -12,9 +12,9 @@ authors = [ license = {file = "../../LICENSE"} requires-python = ">=3.13,<3.14" dependencies = [ - "opentelemetry-instrumentation-sqlalchemy (>=0.51b0,<0.52)", + "opentelemetry-instrumentation-sqlalchemy (>=0.65b0,<0.66)", "pydantic-settings (>=2.7.1,<3.0.0)", - "opentelemetry-instrumentation-fastapi (>=0.51b0,<0.52)", + "opentelemetry-instrumentation-fastapi (>=0.65b0,<0.66)", "policyengine-fastapi", "policyengine-simulation-observability", "policyengine-simulation-contract[modal]", @@ -26,8 +26,7 @@ dependencies = [ "spm-calculator==0.3.1", "tables>=3.10.2", "modal>=0.73.0", - "policyengine-observability[fastapi]>=1.3.0,<2", - "logfire>=3.0.0", + "policyengine-observability[fastapi,google,otlp-grpc]>=3,<4", # Imported directly by the artifact store client; do not rely on the # transitive pin via policyengine. "google-cloud-storage>=2", @@ -51,11 +50,7 @@ modal-simulation-image = [ "pip", "fastapi>=0.115.0", "tables>=3.10.2", - "logfire>=3.0.0", - # logfire imports importlib_metadata unconditionally on Python 3.13 - # but does not declare it (see issue #602). - "importlib-metadata>=8", - "policyengine-observability[fastapi]>=1.3.0,<2", + "policyengine-observability[fastapi,google,otlp-grpc]>=3,<4", # policyengine-us 1.764.6 imports spm_calculator.forecast, which was # removed from later releases still allowed by its broad requirement. "spm-calculator==0.3.1", diff --git a/projects/policyengine-simulation-executor/src/modal/app.py b/projects/policyengine-simulation-executor/src/modal/app.py index 23e0b52a1..274c51694 100644 --- a/projects/policyengine-simulation-executor/src/modal/app.py +++ b/projects/policyengine-simulation-executor/src/modal/app.py @@ -12,21 +12,19 @@ import shlex from pathlib import Path -from policyengine_observability import operation, set_attribute - from src.modal._image_setup import fetch_artifacts, snapshot_models from src.modal.dependency_pins import project_dependency_pin -from policyengine_simulation_observability.logfire_legacy import ( - configure_logfire, - flush_logfire, - legacy_logfire_attributes, - logfire_span, -) from src.modal.logging_redaction import redact_params_for_logging from policyengine_simulation_observability.observability import ( - configure_process_observability, init_process_observability, - process_static_attributes, + modal_image_environment, +) +from policyengine_simulation_observability.telemetry import apply_remote_context +from policyengine_simulation_observability.stages import ( + ANNUAL_IMPACT_STAGES, + BUDGET_WINDOW_STAGES, + SEGMENTED_NATIONAL_STAGES, + Stage, ) from policyengine_simulation_executor.release_bundle import ( get_bundled_country_model_version, @@ -125,8 +123,7 @@ def get_app_name(policyengine_version: str) -> str: gcp_secret = modal.Secret.from_name("gcp-credentials", environment_name="main") data_secret = modal.Secret.from_name("policyengine-data-credentials") hf_secret = modal.Secret.from_name("huggingface-token") -# Legacy Logfire export remains while we evaluate a replacement observability platform. -logfire_secret = modal.Secret.from_name("policyengine-logfire") +OBSERVABILITY_ENV = modal_image_environment() # Only meaningful locally: image definitions are built on the deploying @@ -255,6 +252,7 @@ def build_runtime_simulation_image() -> modal.Image: secrets=[data_secret, hf_secret], ) .env(VERSION_ENV) + .env(OBSERVABILITY_ENV) ) @@ -297,23 +295,19 @@ def build_base_simulation_image() -> modal.Image: def _configure_modal_observability( *, service_role: str, - modal_function_name: str, -) -> dict: - configure_process_observability( +): + return init_process_observability( + service_name=APP_NAME, platform="modal", service_role=service_role, - modal_app_name=APP_NAME, - modal_function_name=modal_function_name, + environment=os.getenv("MODAL_ENVIRONMENT", "local"), + service_version=POLICYENGINE_VERSION, ) - init_process_observability(service_role=service_role) - # Worker operations have no FastAPI adapter to inject static identity - # attributes, so the caller must merge these into its operation attrs. - return process_static_attributes(service_role=service_role) -def _set_modal_call_attributes() -> None: +def _set_modal_call_attributes(runtime) -> None: try: - set_attribute("function_call_id", modal.current_function_call_id()) + runtime.set_context(function_call_id=modal.current_function_call_id()) except Exception: pass @@ -325,48 +319,49 @@ def _set_modal_call_attributes() -> None: timeout=3600, retries=0, max_containers=100, - secrets=[gcp_secret, data_secret, hf_secret, logfire_secret], + secrets=[gcp_secret, data_secret, hf_secret], ) def run_simulation(params: dict) -> dict: """ Execute economic simulation. Imports the snapshotted implementation at runtime. - Emits redacted operation data to both observability systems. + Emits redacted operation data through the shared observability runtime. """ - static_attributes = _configure_modal_observability( - service_role="simulation_worker", - modal_function_name="run_simulation", - ) + runtime = _configure_modal_observability(service_role="simulation_worker") # We deliberately avoid sending full ``params`` or ``result`` blobs to - # either observability system: both can embed signed URLs, reform + # observability: they can embed signed URLs, reform # parameter trees with sensitive policy details, or result payloads - # large enough to blow attribute budgets. The redacted summary keeps - # correlation traceability via run_id while leaving the heavy payload - # in memory. + # large enough to blow attribute budgets. Correlation fields come from + # the validated remote context while the heavy payload stays in memory. redacted_params = { **redact_params_for_logging(params), - **static_attributes, - **legacy_logfire_attributes(), + "modal_app_name": APP_NAME, + "modal_environment": os.getenv("MODAL_ENVIRONMENT", "local"), + "modal_function_name": "run_simulation", } - logfire_enabled = False try: - with operation("run_simulation", flavor="modal_function", **redacted_params): - logfire_enabled = configure_logfire("policyengine-simulation") - _set_modal_call_attributes() - with logfire_span(logfire_enabled, "run_simulation", **redacted_params): - from src.modal.segmented_national import ( - dispatch_run_simulation, - ) - - # Plain national macro requests fan out across region groups - # by default (segmented: false opts out). APP_NAME is this - # deployed app; children spawn into its dedicated - # run_simulation_segment pool. - return dispatch_run_simulation(params, app_name=APP_NAME) + propagated = apply_remote_context(runtime, params) + with runtime.operation( + ANNUAL_IMPACT_STAGES.name(Stage.ANNUAL_EXECUTION), + attributes=redacted_params, + remote_context=propagated, + ): + _set_modal_call_attributes(runtime) + from src.modal.segmented_national import dispatch_run_simulation + + # Plain national macro requests fan out across region groups + # by default (segmented: false opts out). APP_NAME is this + # deployed app; children spawn into its dedicated + # run_simulation_segment pool. + return dispatch_run_simulation( + params, + app_name=APP_NAME, + runtime=runtime, + ) finally: - flush_logfire(logfire_enabled) + runtime.shutdown() @app.function( @@ -376,7 +371,7 @@ def run_simulation(params: dict) -> dict: timeout=3600, retries=0, max_containers=300, - secrets=[gcp_secret, data_secret, hf_secret, logfire_secret], + secrets=[gcp_secret, data_secret, hf_secret], ) def run_simulation_segment(params: dict) -> dict: """One region-group child of a segmented national run. @@ -387,32 +382,28 @@ def run_simulation_segment(params: dict) -> dict: waits on. Calls the monolithic impl directly — children are ``region_group`` requests and never re-segment. """ - static_attributes = _configure_modal_observability( - service_role="simulation_worker", - modal_function_name="run_simulation_segment", - ) + runtime = _configure_modal_observability(service_role="simulation_worker") redacted_params = { **redact_params_for_logging(params), - **static_attributes, - **legacy_logfire_attributes(), + "modal_app_name": APP_NAME, + "modal_environment": os.getenv("MODAL_ENVIRONMENT", "local"), + "modal_function_name": "run_simulation_segment", } - logfire_enabled = False try: - with operation( - "run_simulation_segment", flavor="modal_function", **redacted_params + propagated = apply_remote_context(runtime, params) + with runtime.operation( + SEGMENTED_NATIONAL_STAGES.name(Stage.SEGMENTED_NATIONAL_EXECUTION), + attributes=redacted_params, + remote_context=propagated, ): - logfire_enabled = configure_logfire("policyengine-simulation") - _set_modal_call_attributes() - with logfire_span( - logfire_enabled, "run_simulation_segment", **redacted_params - ): - from policyengine_simulation_executor.simulation_runtime import ( - run_simulation_impl, - ) - - return run_simulation_impl(params) + _set_modal_call_attributes(runtime) + from policyengine_simulation_executor.simulation_runtime import ( + run_simulation_impl, + ) + + return run_simulation_impl(params, runtime=runtime) finally: - flush_logfire(logfire_enabled) + runtime.shutdown() @app.function( @@ -422,36 +413,28 @@ def run_simulation_segment(params: dict) -> dict: timeout=3600, retries=0, max_containers=100, - secrets=[gcp_secret, data_secret, hf_secret, logfire_secret], + secrets=[gcp_secret, data_secret, hf_secret], ) def run_budget_window_batch(params: dict) -> dict: """Execute a multi-year budget-window batch orchestration.""" - static_attributes = _configure_modal_observability( - service_role="budget_window_worker", - modal_function_name="run_budget_window_batch", - ) + runtime = _configure_modal_observability(service_role="budget_window_worker") redacted_params = { **redact_params_for_logging(params), - **static_attributes, - **legacy_logfire_attributes(), + "modal_app_name": APP_NAME, + "modal_environment": os.getenv("MODAL_ENVIRONMENT", "local"), + "modal_function_name": "run_budget_window_batch", } - logfire_enabled = False try: - with operation( - "run_budget_window_batch", - flavor="modal_function", - **redacted_params, + propagated = apply_remote_context(runtime, params) + with runtime.operation( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_EXECUTION), + attributes=redacted_params, + remote_context=propagated, ): - logfire_enabled = configure_logfire("policyengine-simulation") - _set_modal_call_attributes() - with logfire_span( - logfire_enabled, - "run_budget_window_batch", - **redacted_params, - ): - from src.modal.budget_window_batch import run_budget_window_batch_impl - - return run_budget_window_batch_impl(params) + _set_modal_call_attributes(runtime) + from src.modal.budget_window_batch import run_budget_window_batch_impl + + return run_budget_window_batch_impl(params, runtime=runtime) finally: - flush_logfire(logfire_enabled) + runtime.shutdown() diff --git a/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py b/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py index b1c04010c..3707f5dfe 100644 --- a/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py +++ b/projects/policyengine-simulation-executor/src/modal/budget_window_batch.py @@ -5,25 +5,29 @@ from typing import Any import modal -from policyengine_observability import segment, set_attribute +from policyengine_observability import ObservabilityRuntime from src.modal.budget_window_context import build_batch_context from src.modal.budget_window_scheduler import BudgetWindowBatchRunner -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import BUDGET_WINDOW_STAGES, Stage -def run_budget_window_batch_impl(params: dict[str, Any]) -> dict[str, Any]: +def run_budget_window_batch_impl( + params: dict[str, Any], + *, + runtime: ObservabilityRuntime, +) -> dict[str, Any]: from policyengine_simulation_executor.spm import normalize_runtime_spm selection = normalize_runtime_spm(params) if selection is not None: params = {**params, "spm": selection} batch_job_id = modal.current_function_call_id() - set_attribute("batch_job_id", batch_job_id) - with segment(SegmentName.BUDGET_WINDOW_CONTEXT): + runtime.set_context(batch_job_id=batch_job_id) + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_CONTEXT)): context = build_batch_context( params, batch_job_id=batch_job_id, ) - runner = BudgetWindowBatchRunner(context) + runner = BudgetWindowBatchRunner(context, runtime=runtime) return runner.run() diff --git a/projects/policyengine-simulation-executor/src/modal/budget_window_context.py b/projects/policyengine-simulation-executor/src/modal/budget_window_context.py index ce91f8073..03c22cb4b 100644 --- a/projects/policyengine-simulation-executor/src/modal/budget_window_context.py +++ b/projects/policyengine-simulation-executor/src/modal/budget_window_context.py @@ -18,6 +18,7 @@ "max_parallel", "target", "_metadata", + "_observability_context", "_telemetry", } diff --git a/projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py b/projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py index c54bee76c..e487a4c06 100644 --- a/projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py +++ b/projects/policyengine-simulation-executor/src/modal/budget_window_scheduler.py @@ -6,7 +6,7 @@ from typing import Any import modal -from policyengine_observability import segment, set_attribute +from policyengine_observability import ObservabilityRuntime from src.modal.budget_window_context import ( BudgetWindowBatchContext, @@ -32,7 +32,7 @@ ) from policyengine_simulation_contract.spm import spm_error_detail from policyengine_simulation_observability.errors import log_and_redact_exception -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import BUDGET_WINDOW_STAGES, Stage # Polling tuning. The runner busy-loops across child FunctionCall.get(timeout=0) # probes; when no child resolved we sleep before the next probe to stop the @@ -51,13 +51,23 @@ POLL_INTERVAL_SECONDS = POLL_INTERVAL_INITIAL_SECONDS -def serialize_batch_status(state) -> dict[str, Any]: - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): +def serialize_batch_status( + state, + *, + runtime: ObservabilityRuntime, +) -> dict[str, Any]: + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): return build_batch_status_response(state).model_dump(mode="json") -def load_or_create_batch_state(context: BudgetWindowBatchContext): - with segment(SegmentName.BUDGET_WINDOW_STATE_LOAD): +def load_or_create_batch_state( + context: BudgetWindowBatchContext, + *, + runtime: ObservabilityRuntime, +): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_LOAD)): state = get_batch_job_seed(context.batch_job_id) if state is None: state = create_initial_batch_state( @@ -67,7 +77,7 @@ def load_or_create_batch_state(context: BudgetWindowBatchContext): resolved_app_name=context.resolved_app_name, bundle=context.bundle, ) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE)): put_batch_job_seed(state) return state @@ -79,22 +89,26 @@ def __init__( self, context: BudgetWindowBatchContext, *, + runtime: ObservabilityRuntime, modal_module=None, poll_interval_seconds: float = POLL_INTERVAL_INITIAL_SECONDS, poll_interval_max_seconds: float = POLL_INTERVAL_MAX_SECONDS, poll_interval_backoff_factor: float = POLL_INTERVAL_BACKOFF_FACTOR, ): self.context = context + self.runtime = runtime self.modal = modal if modal_module is None else modal_module self.poll_interval_initial_seconds = poll_interval_seconds self.poll_interval_max_seconds = poll_interval_max_seconds self.poll_interval_backoff_factor = poll_interval_backoff_factor # Kept for tests that still read this attribute. self.poll_interval_seconds = poll_interval_seconds - set_attribute("batch_job_id", context.batch_job_id) - set_attribute("resolved_app_name", context.resolved_app_name) - set_attribute("resolved_version", context.resolved_version) - self.state = load_or_create_batch_state(context) + self.runtime.set_context( + batch_job_id=context.batch_job_id, + resolved_app_name=context.resolved_app_name, + resolved_version=context.resolved_version, + ) + self.state = load_or_create_batch_state(context, runtime=runtime) # ``Function.from_name`` is a lazy handle (no RPC); hydration happens # on first spawn, which BUDGET_WINDOW_CHILD_SPAWN already times. self.child_func = self.modal.Function.from_name( @@ -115,7 +129,9 @@ def __init__( def run(self) -> dict[str, Any]: mark_batch_running(self.state) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE) + ): put_batch_job_state(self.state) # Exponential backoff: reset on any progress, double on empty polls. @@ -125,7 +141,7 @@ def run(self) -> dict[str, Any]: self.spawn_until_capacity() progress_made = self.poll_running_children_once() if self.state.status == "failed": - return serialize_batch_status(self.state) + return serialize_batch_status(self.state, runtime=self.runtime) if self.state.running_years and not progress_made: time.sleep(current_sleep) self._backoff_sleep_seconds += current_sleep @@ -149,17 +165,20 @@ def spawn_until_capacity(self) -> None: and self.state.queued_years ): simulation_year = self.state.queued_years[0] - with segment( - SegmentName.BUDGET_WINDOW_CHILD_REQUEST_BUILD, - simulation_year=simulation_year, + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_CHILD_REQUEST_BUILD), + attributes={"simulation_year": simulation_year}, ): child_request = build_child_simulation_request( self.context, simulation_year=simulation_year, ) - with segment( - SegmentName.BUDGET_WINDOW_CHILD_SPAWN, - simulation_year=simulation_year, + child_request.payload["_observability_context"] = ( + self.runtime.capture_context() + ) + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_CHILD_SPAWN), + attributes={"simulation_year": simulation_year}, ): call = self.child_func.spawn(child_request.payload) self.child_handles[simulation_year] = ChildSimulationHandle( @@ -172,7 +191,9 @@ def spawn_until_capacity(self) -> None: year=simulation_year, child_job_id=call.object_id, ) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE) + ): put_batch_job_state(self.state) def poll_running_children_once(self) -> bool: @@ -198,6 +219,7 @@ def poll_running_children_once(self) -> bool: return False redacted = log_and_redact_exception( exc, + runtime=self.runtime, scope="budget_window_child_call", context={ "batch_job_id": self.context.batch_job_id, @@ -215,9 +237,9 @@ def poll_running_children_once(self) -> bool: self._publish_poll_stats() try: - with segment( - SegmentName.BUDGET_WINDOW_RESULT_PARSE, - simulation_year=simulation_year, + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_RESULT_PARSE), + attributes={"simulation_year": simulation_year}, ): annual_impact = extract_annual_impact( simulation_year=simulation_year, @@ -236,6 +258,7 @@ def poll_running_children_once(self) -> bool: return False redacted = log_and_redact_exception( exc, + runtime=self.runtime, scope="budget_window_child_result_parsing", context={ "batch_job_id": self.context.batch_job_id, @@ -253,7 +276,9 @@ def poll_running_children_once(self) -> bool: year=simulation_year, annual_impact=annual_impact, ) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE) + ): put_batch_job_state(self.state) progress_made = True @@ -278,15 +303,11 @@ def resolve_child_handle(self, simulation_year: str) -> ChildSimulationHandle: return resolved_handle def _publish_poll_stats(self) -> None: - set_attribute("child_poll_count", self._child_poll_count) - set_attribute( - "child_poll_ms_total", - round(self._child_poll_seconds * 1000, 1), - ) - set_attribute("backoff_sleep_count", self._backoff_sleep_count) - set_attribute( - "backoff_sleep_ms_total", - round(self._backoff_sleep_seconds * 1000, 1), + self.runtime.set_context( + child_poll_count=self._child_poll_count, + child_poll_ms_total=round(self._child_poll_seconds * 1000, 1), + backoff_sleep_count=self._backoff_sleep_count, + backoff_sleep_ms_total=round(self._backoff_sleep_seconds * 1000, 1), ) def fail_batch_for_child_error( @@ -297,11 +318,15 @@ def fail_batch_for_child_error( ) -> None: mark_child_failed(self.state, year=simulation_year, error=error) mark_batch_failed(self.state, error=error) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE) + ): put_batch_job_state(self.state) def complete_batch(self) -> dict[str, Any]: - with segment(SegmentName.BUDGET_WINDOW_AGGREGATION): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_AGGREGATION) + ): annual_impacts = [ self.state.partial_annual_impacts[simulation_year] for simulation_year in self.state.years @@ -313,6 +338,8 @@ def complete_batch(self) -> dict[str, Any]: annual_impacts=annual_impacts, ) mark_batch_complete(self.state, result=result) - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with self.runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE) + ): put_batch_job_state(self.state) - return serialize_batch_status(self.state) + return serialize_batch_status(self.state, runtime=self.runtime) diff --git a/projects/policyengine-simulation-executor/src/modal/logging_redaction.py b/projects/policyengine-simulation-executor/src/modal/logging_redaction.py index d62854244..6f3f02a89 100644 --- a/projects/policyengine-simulation-executor/src/modal/logging_redaction.py +++ b/projects/policyengine-simulation-executor/src/modal/logging_redaction.py @@ -1,4 +1,4 @@ -"""Payload redaction helpers for structured logs and legacy Logfire spans. +"""Payload redaction helpers for structured observability records. We keep these in a separate module from :mod:`src.modal.app` so they can be unit-tested without instantiating the Modal app (which requires runtime @@ -24,27 +24,19 @@ def redact_params_for_logging(params) -> dict: corresponds to, but strip any field that may contain URLs with signed credentials or arbitrarily large user-submitted parameter trees. - Underscore-prefixed keys (``_telemetry``, ``_metadata``, + Underscore-prefixed keys (``_telemetry``, ``_observability_context``, ``_metadata``, ``_emit_microdata``, and any future internal flag) are dropped: they are internal control/routing fields, and observability backends reject attribute keys that start with an underscore — so leaking one crashes - the span rather than merely over-logging. Correlation ids are surfaced - explicitly below. Non-dict inputs return an empty dict so callers can - splat the result into operation attributes without additional guards. + the span rather than merely over-logging. Non-dict inputs return an empty + dict so callers can splat the result into operation attributes without + additional guards. """ if not isinstance(params, dict): return {} - redacted = { + return { key: value for key, value in params.items() if key not in SENSITIVE_PARAM_KEYS and not key.startswith("_") } - # Surface only the correlation/run ids from telemetry, not the whole - # envelope. - telemetry = params.get("_telemetry") - if isinstance(telemetry, dict): - run_id = telemetry.get("run_id") - if run_id is not None: - redacted["run_id"] = run_id - return redacted diff --git a/projects/policyengine-simulation-executor/src/modal/segmented_national.py b/projects/policyengine-simulation-executor/src/modal/segmented_national.py index 374d2a999..afbbab114 100644 --- a/projects/policyengine-simulation-executor/src/modal/segmented_national.py +++ b/projects/policyengine-simulation-executor/src/modal/segmented_national.py @@ -25,7 +25,7 @@ from typing import Any import modal -from policyengine_observability import segment, set_attribute +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_executor.national_partition import ( national_region_groups, @@ -39,7 +39,10 @@ combine_spm_results, ) from policyengine_simulation_observability.errors import log_and_redact_exception -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import ( + SEGMENTED_NATIONAL_STAGES, + Stage, +) from policyengine_simulation_observability.telemetry import split_internal_payload from src.modal.fanout import build_child_payload, next_backoff @@ -138,12 +141,14 @@ def __init__( params: dict[str, Any], *, app_name: str, + runtime: ObservabilityRuntime, modal_module=None, poll_interval_seconds: float = POLL_INTERVAL_INITIAL_SECONDS, poll_interval_max_seconds: float = POLL_INTERVAL_MAX_SECONDS, ): self.params = params self.app_name = app_name + self.runtime = runtime self.modal = modal if modal_module is None else modal_module self.poll_interval_initial_seconds = poll_interval_seconds self.poll_interval_max_seconds = poll_interval_max_seconds @@ -154,12 +159,14 @@ def __init__( self.groups = groups # Lazy handle, no RPC until first spawn (budget-window precedent). self.child_func = self.modal.Function.from_name(app_name, SEGMENT_FUNCTION_NAME) - set_attribute("national_execution", "segmented") - set_attribute("segmented_group_count", len(self.groups)) - set_attribute("country", self.country) - set_attribute("region", self.country) + self.runtime.set_context( + national_execution="segmented", + segmented_group_count=len(self.groups), + country=self.country, + region=self.country, + ) if params.get("time_period"): - set_attribute("simulation_year", str(params["time_period"])) + self.runtime.set_context(simulation_year=str(params["time_period"])) def run(self) -> dict[str, Any]: country_module = self._country_module() @@ -199,7 +206,7 @@ def _extend_last_group_with_uncovered_states(self, country_module) -> None: "US_NATIONAL_REGION_GROUPS with a measured partition.", uncovered, ) - set_attribute("partition_uncovered_regions", ",".join(uncovered)) + self.runtime.set_context(partition_uncovered_regions=",".join(uncovered)) self.groups[-1] = list(self.groups[-1]) + uncovered def _spawn_all_children(self) -> list[tuple[list[str], Any]]: @@ -207,9 +214,12 @@ def _spawn_all_children(self) -> list[tuple[list[str], Any]]: try: for group in self.groups: payload = build_group_child_payload(self.params, group) - with segment( - SegmentName.SEGMENTED_NATIONAL_CHILD_SPAWN, - region_group="+".join(group), + payload["_observability_context"] = self.runtime.capture_context() + with self.runtime.span( + SEGMENTED_NATIONAL_STAGES.name( + Stage.SEGMENTED_NATIONAL_CHILD_SPAWN + ), + attributes={"region_group": "+".join(group)}, ): call = self.child_func.spawn(payload) handles.append((group, call)) @@ -258,6 +268,7 @@ def _collect(self, handles: list[tuple[list[str], Any]]) -> list[dict]: continue redacted = log_and_redact_exception( exc, + runtime=self.runtime, scope="segmented_national_child", context={"region_group": "+".join(group)}, ) @@ -270,8 +281,8 @@ def _collect(self, handles: list[tuple[list[str], Any]]) -> list[dict]: poll_errors.pop(index, None) progress_made = True # Bounded aggregate instead of one segment per probe (see the - # SegmentName NOTE about long poll loops). - set_attribute("segmented_poll_count", poll_count) + # Stage registry note about long poll loops). + self.runtime.set_context(segmented_poll_count=poll_count) if pending and not progress_made: time.sleep(current_sleep) current_sleep = next_backoff( @@ -299,7 +310,9 @@ def _reduce(self, child_results: list[dict], *, country_module) -> dict[str, Any # The canonical internal-key stripper, plus the opt-out knob. simulation_params, _, _ = split_internal_payload(self.params) simulation_params.pop("segmented", None) - with segment(SegmentName.SEGMENTED_NATIONAL_REDUCE): + with self.runtime.span( + SEGMENTED_NATIONAL_STAGES.name(Stage.SEGMENTED_NATIONAL_REDUCE) + ): output = build_national_output( child_results, country=self.country, @@ -307,6 +320,7 @@ def _reduce(self, child_results: list[dict], *, country_module) -> dict[str, Any country_module=country_module, year=_parse_year(simulation_params), resolved_data_version=None, + runtime=self.runtime, ) output.update( combine_spm_results( @@ -317,17 +331,26 @@ def _reduce(self, child_results: list[dict], *, country_module) -> dict[str, Any ) for key in ("model_version", "data_version"): if output.get(key): - set_attribute(key, str(output[key])) + self.runtime.set_context(**{key: str(output[key])}) return output def run_segmented_national_impl( - params: dict[str, Any], *, app_name: str + params: dict[str, Any], *, app_name: str, runtime: ObservabilityRuntime ) -> dict[str, Any]: - return SegmentedNationalRunner(params, app_name=app_name).run() + return SegmentedNationalRunner( + params, + app_name=app_name, + runtime=runtime, + ).run() -def dispatch_run_simulation(params: dict[str, Any], *, app_name: str) -> dict[str, Any]: +def dispatch_run_simulation( + params: dict[str, Any], + *, + app_name: str, + runtime: ObservabilityRuntime, +) -> dict[str, Any]: """The run_simulation entrypoint's routing: segmented national fan-out for eligible requests, the monolithic path for everything else.""" from policyengine_simulation_executor.spm import normalize_runtime_spm @@ -336,7 +359,11 @@ def dispatch_run_simulation(params: dict[str, Any], *, app_name: str) -> dict[st if selection is not None: params = {**params, "spm": selection} if should_run_segmented_national(params): - return run_segmented_national_impl(params, app_name=app_name) + return run_segmented_national_impl( + params, + app_name=app_name, + runtime=runtime, + ) from policyengine_simulation_executor.simulation_runtime import ( run_simulation_impl, @@ -345,5 +372,5 @@ def dispatch_run_simulation(params: dict[str, Any], *, app_name: str) -> dict[st # Only label runs that are actually national-shaped: children, regional, # and UK requests must not pollute the segmented-vs-monolithic metric. if is_plain_national_macro(params): - set_attribute("national_execution", "monolithic") - return run_simulation_impl(params) + runtime.set_context(national_execution="monolithic") + return run_simulation_impl(params, runtime=runtime) diff --git a/projects/policyengine-simulation-executor/src/modal/smoke_app.py b/projects/policyengine-simulation-executor/src/modal/smoke_app.py index e05234009..164a4971f 100644 --- a/projects/policyengine-simulation-executor/src/modal/smoke_app.py +++ b/projects/policyengine-simulation-executor/src/modal/smoke_app.py @@ -8,8 +8,7 @@ take seconds; after a relock it pays only the bundle install. Runs the imports the deployed workers perform lazily at request time — -``run_simulation_impl``, the budget-window batch, both shared libs, and -the explicit ``import logfire`` (issue #602's crash path). +``run_simulation_impl``, the budget-window batch, and both shared libraries. Usage: uv run modal run --env=staging src/modal/smoke_app.py @@ -35,10 +34,6 @@ def smoke_import_executor() -> dict: import importlib import pkgutil - # The #602 crash path: workers import logfire lazily inside - # configure_logfire; import it explicitly here. - import logfire # noqa: F401 - # Module-level surface of the deployed app (versions resolve from the # baked env layer). importlib.import_module("src.modal.app") diff --git a/projects/policyengine-simulation-executor/src/modal/v2_app.py b/projects/policyengine-simulation-executor/src/modal/v2_app.py index eaa869373..8392b50bf 100644 --- a/projects/policyengine-simulation-executor/src/modal/v2_app.py +++ b/projects/policyengine-simulation-executor/src/modal/v2_app.py @@ -14,6 +14,17 @@ from policyengine_simulation_contract.stage12_bundle import CountryId from policyengine_simulation_contract.stage12_manifest import v2_application_name +from policyengine_simulation_observability.observability import ( + init_process_observability, + modal_image_environment, +) +from policyengine_simulation_observability.stages import ( + STAGE12_CANONICAL_REPORT_STAGES, + STAGE12_SHADOW_REPORT_STAGES, + STAGE12_SIMULATION_STAGES, + Stage, +) +from policyengine_simulation_observability.telemetry import apply_remote_context import modal from policyengine_simulation_executor.stage12_bundle import ( @@ -56,13 +67,11 @@ def _external_assertions(environment: dict[str, str]) -> dict[str, str]: gcp_secret = modal.Secret.from_name("stage12-evaluation-gcp-credentials") data_secret = modal.Secret.from_name("policyengine-data-credentials") hf_secret = modal.Secret.from_name("huggingface-token") -logfire_secret = modal.Secret.from_name("policyengine-logfire") comparison_runtime_secret = modal.Secret.from_name("stage12-evaluation-runtime") worker_secrets = [ gcp_secret, data_secret, hf_secret, - logfire_secret, comparison_runtime_secret, ] @@ -109,6 +118,7 @@ def build_v2_image(countries: tuple[CountryId, ...]) -> modal.Image: ) .env( { + **modal_image_environment(), "POLICYENGINE_DATA_FOLDER": STAGE12_DATA_DIR, "STAGE12_BUNDLE_MANIFEST_SHA256": ( RESOLVED_BUNDLE.bundle_manifest_sha256 @@ -180,12 +190,41 @@ def validate_worker_uk() -> dict: max_containers=10, secrets=worker_secrets, ) -def run_single_simulation_us(payload: dict, context: dict) -> dict: +def run_single_simulation_us( + payload: dict, + context: dict, + observability_context: dict | None = None, +) -> dict: from policyengine_simulation_executor.stage12_runtime import ( run_single_simulation, ) - return run_single_simulation(payload, context, required_country="us") + runtime = init_process_observability( + service_name="policyengine-stage12-us-worker", + service_role="stage12_simulation_worker", + platform="modal", + environment=os.getenv("MODAL_ENVIRONMENT", "local"), + ) + try: + propagated = apply_remote_context( + runtime, {"_observability_context": observability_context} + ) + with runtime.operation( + STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_SIMULATION_EXECUTION), + attributes={ + "runner_name": "stage12", + "simulation_role": payload.get("role"), + }, + remote_context=propagated, + ): + return run_single_simulation( + payload, + context, + required_country="us", + runtime=runtime, + ) + finally: + runtime.shutdown() @app.function( @@ -197,12 +236,41 @@ def run_single_simulation_us(payload: dict, context: dict) -> dict: max_containers=10, secrets=worker_secrets, ) -def run_single_simulation_uk(payload: dict, context: dict) -> dict: +def run_single_simulation_uk( + payload: dict, + context: dict, + observability_context: dict | None = None, +) -> dict: from policyengine_simulation_executor.stage12_runtime import ( run_single_simulation, ) - return run_single_simulation(payload, context, required_country="uk") + runtime = init_process_observability( + service_name="policyengine-stage12-uk-worker", + service_role="stage12_simulation_worker", + platform="modal", + environment=os.getenv("MODAL_ENVIRONMENT", "local"), + ) + try: + propagated = apply_remote_context( + runtime, {"_observability_context": observability_context} + ) + with runtime.operation( + STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_SIMULATION_EXECUTION), + attributes={ + "runner_name": "stage12", + "simulation_role": payload.get("role"), + }, + remote_context=propagated, + ): + return run_single_simulation( + payload, + context, + required_country="uk", + runtime=runtime, + ) + finally: + runtime.shutdown() @app.function( @@ -216,7 +284,12 @@ def run_single_simulation_uk(payload: dict, context: dict) -> dict: max_containers=10, secrets=worker_secrets, ) -def coordinate_report(payload: dict, context: dict, parent: dict) -> dict: +def coordinate_report( + payload: dict, + context: dict, + parent: dict, + observability_context: dict | None = None, +) -> dict: from policyengine_simulation_executor.stage12_runtime import ( coordinate_report as run_report_coordinator, ) @@ -224,10 +297,40 @@ def coordinate_report(payload: dict, context: dict, parent: dict) -> dict: coordinator_invocation_id = modal.current_function_call_id() if coordinator_invocation_id is None: raise RuntimeError("Modal coordinator invocation identifier is unavailable") - return run_report_coordinator( - payload, - context, - parent, - application_name=APP_NAME, - coordinator_invocation_id=coordinator_invocation_id, + runtime = init_process_observability( + service_name="policyengine-stage12-coordinator", + service_role="stage12_report_coordinator", + platform="modal", + environment=os.getenv("MODAL_ENVIRONMENT", "local"), + ) + stage_plan = ( + STAGE12_SHADOW_REPORT_STAGES + if context.get("production_function_call_id") + else STAGE12_CANONICAL_REPORT_STAGES ) + try: + propagated = apply_remote_context( + runtime, {"_observability_context": observability_context} + ) + with runtime.operation( + stage_plan.name(Stage.STAGE12_COORDINATOR_EXECUTION), + attributes={ + "runner_name": "stage12", + "execution_mode": ( + "shadow" + if context.get("production_function_call_id") + else "authoritative" + ), + }, + remote_context=propagated, + ): + return run_report_coordinator( + payload, + context, + parent, + application_name=APP_NAME, + coordinator_invocation_id=coordinator_invocation_id, + runtime=runtime, + ) + finally: + runtime.shutdown() diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/__init__.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/__init__.py index e746e661c..cb8a2744f 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/__init__.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/__init__.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from policyengine_observability import ObservabilityRuntime from .simulation import create_router """ @@ -7,9 +8,9 @@ """ -def initialize(app: FastAPI): +def initialize(app: FastAPI, runtime: ObservabilityRuntime): """ attach all routes to the app and configure them to use the provided SQLModel engine and jwt settings. """ - app.include_router(create_router()) + app.include_router(create_router(runtime)) diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/main.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/main.py index 4c915b7ab..4ede15ff2 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/main.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/main.py @@ -15,8 +15,8 @@ """ specific example instantiation of the app configured by a .env file * in all environments we use sqlite -* observability emits standard structured logs through policyengine-observability; - legacy Logfire export remains while we evaluate a replacement platform. +* observability emits structured logs, traces, and metrics through + policyengine-observability. """ logger = logging.getLogger(__name__) @@ -25,7 +25,10 @@ @asynccontextmanager async def lifespan(app: FastAPI): async with exit.lifespan(): - yield + try: + yield + finally: + runtime.shutdown() app = FastAPI( @@ -33,14 +36,16 @@ async def lifespan(app: FastAPI): title="policyengine-simulation-executor", summary="Policyengine simulation api", ) -init_simulation_observability( +runtime = init_simulation_observability( app, service_name="policyengine-simulation-executor", service_role="api", + platform="other", + environment="local", ) # attach the api defined in the app package -initialize(app=app) +initialize(app=app, runtime=runtime) # attach ping routes health_registry = HealthRegistry() diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py index c13f03e06..46a8e455d 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/segmented_national_reduce.py @@ -25,6 +25,8 @@ import logging from typing import Any +from policyengine_observability import ObservabilityRuntime + import pandas as pd from microdf import MicroDataFrame @@ -118,6 +120,7 @@ def build_national_output( country_module, year: int, resolved_data_version: str | None = None, + runtime: ObservabilityRuntime | None = None, ) -> dict[str, Any]: """The reduce: children's microdata -> full national macro output dict. @@ -154,6 +157,7 @@ def stand_in(dataset) -> PrecomputedSimulation: reform=stand_in(datasets["reform"]), resolved_data_version=resolved_data_version, resolved_region_code=country, + runtime=runtime, ) output = builder.serialize() # The rebuilt in-memory dataset carries no artifact metadata, so the diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/settings.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/settings.py index 66fc9b9f4..09c3407d9 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/settings.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/settings.py @@ -2,11 +2,6 @@ from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import field_validator -from policyengine_fastapi.observability import ( - ObservabilityConfig, - TracerCaptureMode, - parse_header_value_pairs, -) class Environment(Enum): @@ -33,42 +28,8 @@ def strip_environment(cls, v): instance id used by opentelemetry when reporting trace information """ - observability_enabled: bool = False - observability_shadow_mode: bool = True - observability_service_name: str = "policyengine-simulation-executor" - observability_environment: str | None = None - observability_otlp_endpoint: str | None = None - observability_otlp_headers: str = "" - observability_artifact_bucket: str | None = None - observability_artifact_prefix: str = "simulation-observability" - observability_tracer_capture_mode: TracerCaptureMode = TracerCaptureMode.DISABLED - observability_slow_run_threshold_seconds: float = 30.0 - model_config = SettingsConfigDict(env_file=".env") - @field_validator("observability_otlp_headers", mode="before") - @classmethod - def strip_observability_otlp_headers(cls, value): - if isinstance(value, str): - return value.strip() - return value - - @property - def observability(self) -> ObservabilityConfig: - environment = self.observability_environment or self.environment.value - return ObservabilityConfig( - enabled=self.observability_enabled, - shadow_mode=self.observability_shadow_mode, - service_name=self.observability_service_name, - environment=environment, - otlp_endpoint=self.observability_otlp_endpoint, - otlp_headers=parse_header_value_pairs(self.observability_otlp_headers), - artifact_bucket=self.observability_artifact_bucket, - artifact_prefix=self.observability_artifact_prefix, - tracer_capture_mode=self.observability_tracer_capture_mode, - slow_run_threshold_seconds=self.observability_slow_run_threshold_seconds, - ) - @lru_cache def get_settings(): diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation.py index 309f44315..884b4b3fe 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_contract.spm import spm_error_detail from policyengine_simulation_executor.simulation_runtime import run_simulation_impl @@ -13,7 +14,7 @@ logger = logging.getLogger(__file__) -def create_router(): +def create_router(runtime: ObservabilityRuntime): router = APIRouter() @router.post("/simulate/economy/comparison", response_model=EconomyComparison) @@ -25,7 +26,7 @@ async def simulate( payload = parameters.model_dump(mode="json", exclude_none=True) if parameters.spm is not None: payload["spm"] = parameters.spm.model_dump(mode="json") - result = run_simulation_impl(payload) + result = run_simulation_impl(payload, runtime=runtime) except ValueError as exc: detail = spm_error_detail(exc) if detail: diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_builder.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_builder.py index 886b023a5..9d0d442d9 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_builder.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_output_builder.py @@ -2,10 +2,11 @@ from __future__ import annotations +from contextlib import nullcontext from dataclasses import dataclass, field from typing import Any -from policyengine_observability import segment +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_executor import simulation_output_budget from policyengine_simulation_executor import simulation_output_cliff @@ -14,7 +15,10 @@ from policyengine_simulation_executor import simulation_output_inequality from policyengine_simulation_executor import simulation_output_labor from policyengine_simulation_executor import simulation_output_poverty -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import ( + ANNUAL_IMPACT_STAGES, + Stage, +) from policyengine_simulation_executor.release_bundle import get_country_release_bundle from policyengine_simulation_executor.simulation_macro_output import ( BudgetaryImpact, @@ -44,15 +48,19 @@ class SimulationOutputBuilder: reform: Any resolved_data_version: str | None = None resolved_region_code: str | None = None + runtime: ObservabilityRuntime | None = None _analysis: Any = field(default=None, init=False) def __post_init__(self) -> None: self.country = self.country.lower() + def _span(self, name: str): + return self.runtime.span(name) if self.runtime is not None else nullcontext() + @property def analysis(self) -> Any: if self._analysis is None: - with segment(SegmentName.ECONOMIC_IMPACT_ANALYSIS): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.ECONOMIC_IMPACT_ANALYSIS)): self._analysis = self.country_module.economic_impact_analysis( self.baseline, self.reform, @@ -64,7 +72,7 @@ def _include_cliff_impacts(self) -> bool: return self.simulation_params.get("include_cliffs") is True def build(self) -> SingleYearMacroOutput: - with segment(SegmentName.SIMULATION_OUTPUT_BUILD): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.SIMULATION_OUTPUT_BUILD)): poverty_outputs = self._build_poverty_outputs() wealth_decile = getattr(self.analysis, "wealth_decile_impacts", None) intra_wealth_decile = getattr( @@ -96,44 +104,46 @@ def build(self) -> SingleYearMacroOutput: ) def serialize(self) -> dict[str, Any]: - with segment(SegmentName.CALCULATION): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.CALCULATION)): output = self.build() - with segment(SegmentName.RESPONSE_SERIALIZATION): - with segment(SegmentName.SIMULATION_OUTPUT_MODEL_DUMP): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.RESPONSE_SERIALIZATION)): + with self._span( + ANNUAL_IMPACT_STAGES.name(Stage.SIMULATION_OUTPUT_MODEL_DUMP) + ): return output.model_dump(mode="json") def _build_detailed_budget(self) -> DetailedBudgetOutput: - with segment(SegmentName.OUTPUT_DETAILED_BUDGET): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_DETAILED_BUDGET)): return simulation_output_budget.build_detailed_budget(self.analysis) def _build_decile(self) -> DecileOutput: - with segment(SegmentName.OUTPUT_DECILE): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_DECILE)): return simulation_output_distribution.build_decile(self.analysis) def _build_inequality(self) -> InequalityOutput: - with segment(SegmentName.OUTPUT_INEQUALITY): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_INEQUALITY)): return simulation_output_inequality.build_inequality(self.analysis) def _build_budgetary_impact(self) -> BudgetaryImpact: - with segment(SegmentName.OUTPUT_BUDGETARY_IMPACT): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_BUDGETARY_IMPACT)): return simulation_output_budget.build_budgetary_impact( self.country, self.baseline, self.reform ) def _build_poverty_outputs(self) -> PovertyModuleOutputs: - with segment(SegmentName.OUTPUT_POVERTY): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_POVERTY)): return simulation_output_poverty.build_poverty_outputs( self.country, self.baseline, self.reform, self.analysis ) def _build_intra_decile_output(self) -> IntraDecileOutput: - with segment(SegmentName.OUTPUT_INTRA_DECILE): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_INTRA_DECILE)): return simulation_output_distribution.build_intra_decile_output( self.baseline, self.reform ) def _build_wealth_decile(self, wealth_decile: Any) -> DecileOutput | None: - with segment(SegmentName.OUTPUT_WEALTH_DECILE): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_WEALTH_DECILE)): return simulation_output_distribution.build_wealth_decile( self.country, wealth_decile ) @@ -141,17 +151,17 @@ def _build_wealth_decile(self, wealth_decile: Any) -> DecileOutput | None: def _build_intra_wealth_decile( self, intra_wealth_decile: Any ) -> IntraDecileOutput | None: - with segment(SegmentName.OUTPUT_INTRA_WEALTH_DECILE): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_INTRA_WEALTH_DECILE)): return simulation_output_distribution.build_intra_wealth_decile( self.country, intra_wealth_decile ) def _build_labor_supply_response(self) -> LaborSupplyResponseOutput | None: - with segment(SegmentName.OUTPUT_LABOR_SUPPLY): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_LABOR_SUPPLY)): return simulation_output_labor.build_labor_supply_response(self.analysis) def _build_cliff_impact(self) -> CliffImpactOutput | None: - with segment(SegmentName.OUTPUT_CLIFF): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_CLIFF)): return simulation_output_cliff.build_cliff_impact(self.analysis) def _build_geographic_impact_output( @@ -211,7 +221,7 @@ def _build_poverty_by_race_output( def _build_congressional_district_impact( self, ) -> CongressionalDistrictImpactOutput | None: - with segment(SegmentName.OUTPUT_CONGRESSIONAL_DISTRICT): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_CONGRESSIONAL_DISTRICT)): return simulation_output_geographic.build_congressional_district_impact( self.country, self.baseline, @@ -220,23 +230,23 @@ def _build_congressional_district_impact( ) def _build_uk_constituency_impact(self) -> GeographicImpactOutput | None: - with segment(SegmentName.OUTPUT_UK_CONSTITUENCY): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_UK_CONSTITUENCY)): return simulation_output_geographic.build_uk_constituency_impact( self.country, self.baseline, self.reform ) def _build_uk_local_authority_impact(self) -> GeographicImpactOutput | None: - with segment(SegmentName.OUTPUT_UK_LOCAL_AUTHORITY): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_UK_LOCAL_AUTHORITY)): return simulation_output_geographic.build_uk_local_authority_impact( self.country, self.baseline, self.reform ) def _model_version(self) -> str: - with segment(SegmentName.OUTPUT_MODEL_VERSION): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_MODEL_VERSION)): return str(getattr(self.country_module.model, "version", "")) def _data_version(self) -> str: - with segment(SegmentName.OUTPUT_DATA_VERSION): + with self._span(ANNUAL_IMPACT_STAGES.name(Stage.OUTPUT_DATA_VERSION)): if self.resolved_data_version: return str(self.resolved_data_version) if self.simulation_params.get("data_version"): diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py index 73997ad8f..61ee93170 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/simulation_runtime.py @@ -17,10 +17,13 @@ from importlib import import_module from typing import Any, Iterator -from policyengine_observability import segment, set_attribute +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_contract.dataset_uri import runtime_dataset_uri -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import ( + ANNUAL_IMPACT_STAGES, + Stage, +) from policyengine_simulation_executor.release_bundle import ( get_country_release_bundle, resolve_bundle_dataset_name, @@ -73,7 +76,9 @@ def _normalize_credentials_blob(creds_json: str) -> str: @contextlib.contextmanager -def setup_gcp_credentials() -> Iterator[None]: +def setup_gcp_credentials( + runtime: ObservabilityRuntime | None = None, +) -> Iterator[None]: """ Set up GCP credentials from environment variable. @@ -88,7 +93,12 @@ def setup_gcp_credentials() -> Iterator[None]: previous = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") creds_file = None try: - with segment(SegmentName.CREDENTIAL_SETUP): + credential_span = ( + runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.CREDENTIAL_SETUP)) + if runtime is not None + else contextlib.nullcontext() + ) + with credential_span: # Log available GCP-related env vars for debugging. gcp_vars = { k: v[:50] + "..." if len(v) > 50 else v @@ -135,7 +145,11 @@ def setup_gcp_credentials() -> Iterator[None]: creds_file.close() -def run_simulation_impl(params: dict) -> dict: +def run_simulation_impl( + params: dict, + *, + runtime: ObservabilityRuntime, +) -> dict: """ Execute economic simulation. @@ -144,9 +158,9 @@ def run_simulation_impl(params: dict) -> dict: """ # Set up GCP credentials if needed. The credentials temp file is # cleaned up on exit so we never leave signed JSON material on disk. - with setup_gcp_credentials(): + with setup_gcp_credentials(runtime): try: - return _run_simulation_impl_core(params) + return _run_simulation_impl_core(params, runtime=runtime) except ValueError as exc: # Gateway images intentionally do not install country packages. # Transport the public error through the shared contract instead. @@ -192,8 +206,7 @@ def _normalise_policy(policy: dict[str, Any] | None) -> dict[str, Any] | None: def _resolve_dataset_reference(country: str) -> str: - with segment(SegmentName.DATASET_RESOLUTION): - return resolve_bundle_dataset_name(country, None) + return resolve_bundle_dataset_name(country, None) def _normalise_region_code(country: str, region: Any) -> str: @@ -535,8 +548,12 @@ def _build_simulation( ) -def _run_simulation_impl_core(params: dict) -> dict: - with segment(SegmentName.REQUEST_PARSE): +def _run_simulation_impl_core( + params: dict, + *, + runtime: ObservabilityRuntime, +) -> dict: + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.REQUEST_PARSE)): simulation_params, telemetry, metadata = split_internal_payload(params) metadata = metadata or {} from policyengine_simulation_executor.spm import ( @@ -549,46 +566,50 @@ def _run_simulation_impl_core(params: dict) -> dict: simulation_params["spm"] = selection logger.info( - "Starting simulation for country=%s run_id=%s process_id=%s", + "Starting simulation for country=%s submission_claim_id=%s", simulation_params.get("country", "unknown"), - getattr(telemetry, "run_id", None), - getattr(telemetry, "process_id", None), + getattr(telemetry, "submission_claim_id", None), ) if metadata: logger.info("Received simulation metadata keys: %s", sorted(metadata)) country = simulation_params.get("country", "us").lower() _set_runtime_attributes( + runtime=runtime, simulation_params=simulation_params, telemetry=telemetry, metadata=metadata, country=country, ) - with segment(SegmentName.COUNTRY_MODULE_LOAD): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.COUNTRY_MODULE_LOAD)): country_module = _country_module(country) - with segment(SegmentName.REGION_RESOLUTION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.REGION_RESOLUTION)): region_resolution = _resolve_region( country_module=country_module, country=country, params=simulation_params, ) - set_attribute("region", region_resolution.code) - dataset_selection = _resolve_dataset_selection( - simulation_params, region_resolution=region_resolution - ) - with segment(SegmentName.DATASET_LOAD): + runtime.set_context(region=region_resolution.code) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.DATASET_RESOLUTION)): + dataset_selection = _resolve_dataset_selection( + simulation_params, region_resolution=region_resolution + ) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.DATASET_LOAD)): dataset = _load_dataset( simulation_params, selection=dataset_selection, country_module=country_module, ) - with segment(SegmentName.POLICY_NORMALIZATION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.POLICY_NORMALIZATION)): baseline_policy = _normalise_policy(simulation_params.get("baseline")) reform_policy = _normalise_policy(simulation_params.get("reform")) logger.info("Initialising baseline and reform simulations") - with segment(SegmentName.SIMULATION_BUILD, simulation_kind="baseline"): + with runtime.span( + ANNUAL_IMPACT_STAGES.name(Stage.SIMULATION_BUILD), + attributes={"simulation_kind": "baseline"}, + ): baseline = _build_simulation( simulation_params, dataset=dataset, @@ -597,7 +618,10 @@ def _run_simulation_impl_core(params: dict) -> dict: scoping_strategy=region_resolution.scoping_strategy, region_code=region_resolution.code, ) - with segment(SegmentName.SIMULATION_BUILD, simulation_kind="reform"): + with runtime.span( + ANNUAL_IMPACT_STAGES.name(Stage.SIMULATION_BUILD), + attributes={"simulation_kind": "reform"}, + ): reform = _build_simulation( simulation_params, dataset=dataset, @@ -617,6 +641,7 @@ def _run_simulation_impl_core(params: dict) -> dict: reform=reform, resolved_data_version=None, resolved_region_code=region_resolution.code, + runtime=runtime, ) output = builder.serialize() output.update( @@ -628,7 +653,7 @@ def _run_simulation_impl_core(params: dict) -> dict: # (hit / incomplete / miss) is known for deterministic-id baselines. artifact_outcome = getattr(baseline, "artifact_outcome", None) if artifact_outcome is not None: - set_attribute("baseline_artifact", artifact_outcome) + runtime.set_context(baseline_artifact=artifact_outcome) logger.info("Baseline artifact outcome: %s", artifact_outcome) logger.info("Comparison complete") if params.get("_emit_microdata"): @@ -642,24 +667,26 @@ def _run_simulation_impl_core(params: dict) -> dict: def _set_runtime_attributes( *, + runtime: ObservabilityRuntime, simulation_params: dict[str, Any], telemetry, metadata: dict[str, Any], country: str, ) -> None: - set_attribute("country", country) - set_attribute("scope", simulation_params.get("scope")) - set_attribute("simulation_year", _parse_year(simulation_params)) - set_attribute("run_id", getattr(telemetry, "run_id", None)) - set_attribute("process_id", getattr(telemetry, "process_id", None)) - set_attribute("request_id", getattr(telemetry, "request_id", None)) - set_attribute("geography_code", getattr(telemetry, "geography_code", None)) - set_attribute("geography_type", getattr(telemetry, "geography_type", None)) - - set_attribute("resolved_version", metadata.get("resolved_version")) - set_attribute("resolved_app_name", metadata.get("resolved_app_name")) + runtime.set_context( + country=country, + scope=simulation_params.get("scope"), + simulation_year=_parse_year(simulation_params), + submission_claim_id=getattr(telemetry, "submission_claim_id", None), + geography_code=getattr(telemetry, "geography_code", None), + geography_type=getattr(telemetry, "geography_type", None), + resolved_version=metadata.get("resolved_version"), + resolved_app_name=metadata.get("resolved_app_name"), + ) bundle = metadata.get("policyengine_bundle") if isinstance(bundle, dict): - set_attribute("policyengine_version", bundle.get("policyengine_version")) - set_attribute("model_version", bundle.get("model_version")) - set_attribute("data_version", bundle.get("data_version")) + runtime.set_context( + policyengine_version=bundle.get("policyengine_version"), + model_version=bundle.get("model_version"), + data_version=bundle.get("data_version"), + ) diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_qualification.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_qualification.py index c124fc3fe..9c5425d02 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_qualification.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_qualification.py @@ -8,6 +8,14 @@ from typing import Any, Literal, cast import pandas as pd +from policyengine_observability import ( + DeploymentIdentity, + LoggingConfig, + ObservabilityConfig, + ObservabilityRuntime, + OTelConfig, + ServiceIdentity, +) from pydantic import BaseModel, ConfigDict from policyengine_simulation_contract.stage12_execution import ( @@ -64,6 +72,27 @@ class Stage12ParityReceipt(BaseModel): AggregateBuilder = Callable[..., dict[str, Any]] +def _local_qualification_runtime() -> ObservabilityRuntime: + """Create a local runtime that cannot send qualification data remotely.""" + + return ObservabilityRuntime( + ObservabilityConfig( + service=ServiceIdentity( + name="policyengine-stage12-parity", + namespace="policyengine.simulation-v2", + version="local", + role="qualification", + ), + deployment=DeploymentIdentity( + environment="local", + platform="local", + ), + logging=LoggingConfig(destinations=()), + otel=OTelConfig(enabled=False), + ) + ) + + def _validate_matching_input( existing_request: Mapping[str, Any], report: ReportExecutionInput, @@ -191,12 +220,19 @@ def qualify_report_parity( report = ReportExecutionInput.model_validate(report_payload) _validate_matching_input(existing_request, report) + local_runtime: ObservabilityRuntime | None = None if existing_runner is None: from policyengine_simulation_executor.simulation_runtime import ( run_simulation_impl, ) - existing_runner = run_simulation_impl + local_runtime = _local_qualification_runtime() + + def run_existing(request: dict[str, Any]) -> Mapping[str, Any]: + assert local_runtime is not None + return run_simulation_impl(request, runtime=local_runtime) + + existing_runner = run_existing if single_simulation_runner is None: from policyengine_simulation_executor.stage12_runtime import ( calculate_simulation_frames, @@ -212,7 +248,11 @@ def qualify_report_parity( request_with_microdata = dict(existing_request) request_with_microdata["_emit_microdata"] = True - existing_result = dict(existing_runner(request_with_microdata)) + try: + existing_result = dict(existing_runner(request_with_microdata)) + finally: + if local_runtime is not None: + local_runtime.shutdown() microdata = existing_result.pop("_microdata", None) existing_baseline = _existing_frames(microdata, side="baseline") existing_reform = _existing_frames(microdata, side="reform") diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/comparison.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/comparison.py index 10a8c54a0..99055a349 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/comparison.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/comparison.py @@ -3,8 +3,11 @@ from __future__ import annotations import logging +from contextlib import nullcontext from datetime import UTC, datetime from typing import Any +from policyengine_observability import ObservabilityRuntime +from policyengine_simulation_observability.stages import Stage, StagePlan from policyengine_simulation_contract.stage12_execution import ( ComparisonReportRecord, @@ -29,6 +32,8 @@ def compare_completed_report( store: ComparisonStore, artifacts: Stage12ArtifactStore, invoker: ChildInvoker, + runtime: ObservabilityRuntime | None = None, + stage_plan: StagePlan | None = None, ) -> None: """Persist an isolated production/Stage 12 comparison after aggregation.""" @@ -36,34 +41,40 @@ def compare_completed_report( if production_job_id is None: return try: - comparing_at = datetime.now(UTC) - comparing = store.replace_report_result_comparison( - parent.model_copy( - update={ - "comparison_status": ResultComparisonStatus.RUNNING, - "comparison_output_uri": None, - "comparison_output_sha256": None, - "comparison_schema_version": None, - "comparison_completed_at": None, - "comparison_error_code": None, - "comparison_error_summary": None, - "updated_at": comparing_at, - } - ) - ) - production_result = invoker.restore(production_job_id).get( - timeout=PRODUCTION_RESULT_WAIT_TIMEOUT_SECONDS - ) - comparison = compare_results( - evaluation_id=parent.evaluation_id, - production_job_id=production_job_id, - production_result=production_result, - stage12_result=aggregate.get("result"), - ) - comparison_artifact = artifacts.write_comparison( - prefix=context.artifact_prefix, - payload=comparison.model_dump(mode="json"), + comparison_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_RESULT_COMPARISON)) + if runtime is not None and stage_plan is not None + else nullcontext() ) + with comparison_span: + comparing_at = datetime.now(UTC) + comparing = store.replace_report_result_comparison( + parent.model_copy( + update={ + "comparison_status": ResultComparisonStatus.RUNNING, + "comparison_output_uri": None, + "comparison_output_sha256": None, + "comparison_schema_version": None, + "comparison_completed_at": None, + "comparison_error_code": None, + "comparison_error_summary": None, + "updated_at": comparing_at, + } + ) + ) + production_result = invoker.restore(production_job_id).get( + timeout=PRODUCTION_RESULT_WAIT_TIMEOUT_SECONDS + ) + comparison = compare_results( + evaluation_id=parent.evaluation_id, + production_job_id=production_job_id, + production_result=production_result, + stage12_result=aggregate.get("result"), + ) + comparison_artifact = artifacts.write_comparison( + prefix=context.artifact_prefix, + payload=comparison.model_dump(mode="json"), + ) comparison_status = ( ResultComparisonStatus.MATCHED if comparison.status == "matched" @@ -84,7 +95,7 @@ def compare_completed_report( logger.info( "stage12_result_comparison_completed", extra={ - "comparison_run_id": str(parent.evaluation_id), + "evaluation_id": str(parent.evaluation_id), "production_job_id": production_job_id, "comparison_status": comparison_status.value, "difference_count": comparison.difference_count, @@ -120,14 +131,14 @@ def compare_completed_report( logger.exception( "stage12_result_comparison_state_update_failed", extra={ - "comparison_run_id": str(parent.evaluation_id), + "evaluation_id": str(parent.evaluation_id), "production_job_id": production_job_id, }, ) logger.error( "stage12_result_comparison_failed", extra={ - "comparison_run_id": str(parent.evaluation_id), + "evaluation_id": str(parent.evaluation_id), "production_job_id": production_job_id, "error_type": type(error).__name__, }, diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/coordination.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/coordination.py index 67b4d9f90..9761ec50e 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/coordination.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/coordination.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import nullcontext from datetime import UTC, datetime from hashlib import sha256 from typing import Any @@ -20,6 +21,13 @@ SimulationExecutionInput, Stage12InvocationContext, ) +from policyengine_observability import ObservabilityRuntime +from policyengine_simulation_observability.stages import ( + STAGE12_CANONICAL_REPORT_STAGES, + STAGE12_SHADOW_REPORT_STAGES, + Stage, + StagePlan, +) from policyengine_simulation_executor.stage12_artifacts import ( Stage12ArtifactStore, @@ -179,6 +187,7 @@ def coordinate_report( artifacts: Stage12ArtifactStore | None = None, invoker: ChildInvoker | None = None, aggregator: Callable[..., dict[str, Any]] = build_aggregate_report, + runtime: ObservabilityRuntime | None = None, ) -> dict[str, Any]: report = ReportExecutionInput.model_validate(payload) context = Stage12InvocationContext.model_validate(context_payload) @@ -197,11 +206,22 @@ def coordinate_report( persistence = store or runtime_store() artifact_storage = artifacts or artifact_store() child_invoker = invoker or ModalChildInvoker() - parent, should_execute = _claim_parent( - submitted=submitted_parent, - coordinator_invocation_id=coordinator_invocation_id, - store=persistence, + stage_plan: StagePlan = ( + STAGE12_SHADOW_REPORT_STAGES + if context.production_function_call_id is not None + else STAGE12_CANONICAL_REPORT_STAGES + ) + claim_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_COORDINATOR_CLAIM)) + if runtime is not None + else nullcontext() ) + with claim_span: + parent, should_execute = _claim_parent( + submitted=submitted_parent, + coordinator_invocation_id=coordinator_invocation_id, + store=persistence, + ) if not should_execute: return { "deduplicated": True, @@ -224,16 +244,22 @@ def coordinate_report( descriptors: dict[str, SimulationArtifactDescriptor] = {} calls: dict[str, ChildCall] = {} try: - children = { - simulation.role: persistence.create_or_resolve_simulation( - _child_record( - simulation=simulation, - context=context, - function_name=function_name, - ) - ).record - for simulation in simulations - } + child_state_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_CHILD_STATE_CREATE)) + if runtime is not None + else nullcontext() + ) + with child_state_span: + children = { + simulation.role: persistence.create_or_resolve_simulation( + _child_record( + simulation=simulation, + context=context, + function_name=function_name, + ) + ).record + for simulation in simulations + } for simulation in simulations: child = children[simulation.role] if child.status is ComparisonRunLifecycleStatus.SUCCEEDED: @@ -278,13 +304,26 @@ def coordinate_report( ) ) try: - call = child_invoker.spawn( - application_name=application_name, - function_name=function_name, - environment=context.modal_environment, - simulation=simulation.model_dump(mode="json"), - context=context.model_dump(mode="json"), + dispatch_span = ( + runtime.span( + stage_plan.name(Stage.STAGE12_CHILD_DISPATCH), + attributes={"simulation_role": simulation.role.value}, + ) + if runtime is not None + else nullcontext() ) + with dispatch_span: + child_observability_context = ( + runtime.capture_context() if runtime is not None else None + ) + call = child_invoker.spawn( + application_name=application_name, + function_name=function_name, + environment=context.modal_environment, + simulation=simulation.model_dump(mode="json"), + context=context.model_dump(mode="json"), + observability_context=child_observability_context, + ) except Exception as error: failed_at = datetime.now(UTC) latest = persistence.get_simulation(simulation.simulation_execution_id) @@ -311,9 +350,18 @@ def coordinate_report( # Every required call has been started before the coordinator waits. for role, call in calls.items(): try: - descriptors[role] = SimulationArtifactDescriptor.model_validate( - call.get(timeout=SIMULATION_WAIT_TIMEOUT_SECONDS) + wait_span = ( + runtime.span( + stage_plan.name(Stage.STAGE12_CHILD_WAIT), + attributes={"simulation_role": role}, + ) + if runtime is not None + else nullcontext() ) + with wait_span: + descriptors[role] = SimulationArtifactDescriptor.model_validate( + call.get(timeout=SIMULATION_WAIT_TIMEOUT_SECONDS) + ) except Exception as error: simulation = next( item for item in simulations if item.role.value == role @@ -336,23 +384,41 @@ def coordinate_report( baseline = descriptors["baseline"] reform = descriptors["reform"] validate_aligned_outputs(report, baseline, reform) - baseline_payload = artifact_storage.read(baseline.artifact.uri) - reform_payload = artifact_storage.read(reform.artifact.uri) + artifact_read_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_ARTIFACT_READ)) + if runtime is not None + else nullcontext() + ) + with artifact_read_span: + baseline_payload = artifact_storage.read(baseline.artifact.uri) + reform_payload = artifact_storage.read(reform.artifact.uri) if sha256(baseline_payload).hexdigest() != baseline.artifact.content_sha256: raise ValueError("baseline artifact digest mismatch") if sha256(reform_payload).hexdigest() != reform.artifact.content_sha256: raise ValueError("reform artifact digest mismatch") - aggregate = aggregator( - report=report, - baseline_frames=deserialize_simulation_frames(baseline_payload), - reform_frames=deserialize_simulation_frames(reform_payload), - baseline_descriptor=baseline, - reform_descriptor=reform, + aggregation_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_AGGREGATION)) + if runtime is not None + else nullcontext() ) - aggregate_artifact = artifact_storage.write_aggregate( - prefix=context.artifact_prefix, - payload=aggregate, + with aggregation_span: + aggregate = aggregator( + report=report, + baseline_frames=deserialize_simulation_frames(baseline_payload), + reform_frames=deserialize_simulation_frames(reform_payload), + baseline_descriptor=baseline, + reform_descriptor=reform, + ) + aggregate_write_span = ( + runtime.span(stage_plan.name(Stage.STAGE12_AGGREGATE_ARTIFACT_WRITE)) + if runtime is not None + else nullcontext() ) + with aggregate_write_span: + aggregate_artifact = artifact_storage.write_aggregate( + prefix=context.artifact_prefix, + payload=aggregate, + ) descriptor = AggregateReportArtifactDescriptor( evaluation_id=report.evaluation_id, artifact=aggregate_artifact, @@ -381,6 +447,8 @@ def coordinate_report( store=persistence, artifacts=artifact_storage, invoker=child_invoker, + runtime=runtime, + stage_plan=stage_plan, ) return descriptor.model_dump(mode="json") # Persist any coordinator, child-call, aggregation, or artifact failure diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/dependencies.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/dependencies.py index 5818c2128..f2ce21ac3 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/dependencies.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/dependencies.py @@ -74,6 +74,7 @@ def spawn( environment: str, simulation: dict[str, Any], context: dict[str, Any], + observability_context: dict[str, Any] | None, ) -> ChildCall: ... def restore(self, invocation_id: str) -> ChildCall: ... @@ -88,6 +89,7 @@ def spawn( environment: str, simulation: dict[str, Any], context: dict[str, Any], + observability_context: dict[str, Any] | None, ) -> ChildCall: from importlib import import_module @@ -97,7 +99,7 @@ def spawn( function_name, environment_name=environment, ) - return function.spawn(simulation, context) + return function.spawn(simulation, context, observability_context) def restore(self, invocation_id: str) -> ChildCall: from importlib import import_module diff --git a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py index d064cf7e5..876ce6b1a 100644 --- a/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py +++ b/projects/policyengine-simulation-executor/src/policyengine_simulation_executor/stage12_runtime/simulation.py @@ -3,12 +3,14 @@ from __future__ import annotations from collections.abc import Callable, Mapping +from contextlib import nullcontext from dataclasses import dataclass from datetime import UTC, datetime from hashlib import sha256 from typing import Any import pandas as pd +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_contract.stage12_bundle import CountryId from policyengine_simulation_contract.stage12_execution import ( ComparisonRunLifecycleStatus, @@ -23,6 +25,10 @@ canonical_json_bytes, ) from policyengine_simulation_executor.stage12_bundle import load_stage12_bundle +from policyengine_simulation_observability.stages import ( + STAGE12_SIMULATION_STAGES, + Stage, +) from .dependencies import ComparisonStore, artifact_store, runtime_store @@ -94,6 +100,8 @@ def _require_installed_bundle(simulation: SimulationExecutionInput) -> None: def calculate_simulation_frames( simulation: SimulationExecutionInput, + *, + runtime: ObservabilityRuntime | None = None, ) -> SimulationCalculation: """Run exactly one policy and return its complete entity output tables.""" @@ -120,31 +128,79 @@ def calculate_simulation_frames( setup_gcp_credentials, ) - with setup_gcp_credentials(): - country_module = _country_module(country) - region = _resolve_region( - country_module=country_module, - country=country, - params=params, + credential_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.CREDENTIAL_SETUP)) + if runtime is not None + else nullcontext() + ) + with credential_span, setup_gcp_credentials(): + country_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.COUNTRY_MODULE_LOAD)) + if runtime is not None + else nullcontext() ) - dataset_selection = _resolve_dataset_selection( - params, - region_resolution=region, + with country_span: + country_module = _country_module(country) + region_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.REGION_RESOLUTION)) + if runtime is not None + else nullcontext() ) - dataset = _load_dataset( - params, - selection=dataset_selection, - country_module=country_module, + with region_span: + region = _resolve_region( + country_module=country_module, + country=country, + params=params, + ) + dataset_resolution_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.DATASET_RESOLUTION)) + if runtime is not None + else nullcontext() ) - model = _build_simulation( - params, - dataset=dataset, - dataset_selection=dataset_selection, - policy=_normalise_policy(simulation.policy), - scoping_strategy=region.scoping_strategy, - region_code=region.code, + with dataset_resolution_span: + dataset_selection = _resolve_dataset_selection( + params, + region_resolution=region, + ) + dataset_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.DATASET_LOAD)) + if runtime is not None + else nullcontext() ) - model.ensure() + with dataset_span: + dataset = _load_dataset( + params, + selection=dataset_selection, + country_module=country_module, + ) + policy_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.POLICY_NORMALIZATION)) + if runtime is not None + else nullcontext() + ) + with policy_span: + policy = _normalise_policy(simulation.policy) + build_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.SIMULATION_BUILD)) + if runtime is not None + else nullcontext() + ) + with build_span: + model = _build_simulation( + params, + dataset=dataset, + dataset_selection=dataset_selection, + policy=policy, + scoping_strategy=region.scoping_strategy, + region_code=region.code, + ) + calculation_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_CALCULATION)) + if runtime is not None + else nullcontext() + ) + with calculation_span: + model.ensure() output_data = getattr(getattr(model, "output_dataset", None), "data", None) entity_data = getattr(output_data, "entity_data", None) if not isinstance(entity_data, Mapping): @@ -231,6 +287,7 @@ def run_single_simulation( [SimulationExecutionInput], Mapping[str, pd.DataFrame] | SimulationCalculation, ] = calculate_simulation_frames, + runtime: ObservabilityRuntime | None = None, ) -> dict[str, Any]: simulation = SimulationExecutionInput.model_validate(payload) context = Stage12InvocationContext.model_validate(context_payload) @@ -252,23 +309,46 @@ def run_single_simulation( ) persistence.replace_simulation(running) try: - artifact_storage.write_input( - prefix=context.artifact_prefix, - simulation=simulation, + input_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_INPUT_WRITE)) + if runtime is not None + else nullcontext() ) - calculated = calculator(simulation) + with input_span: + artifact_storage.write_input( + prefix=context.artifact_prefix, + simulation=simulation, + ) + if calculator is calculate_simulation_frames: + calculated = calculate_simulation_frames(simulation, runtime=runtime) + else: + calculation_span = ( + runtime.span(STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_CALCULATION)) + if runtime is not None + else nullcontext() + ) + with calculation_span: + calculated = calculator(simulation) if isinstance(calculated, SimulationCalculation): frames = calculated.frames calculation_provenance = calculated.calculation_provenance else: frames = calculated calculation_provenance = None - descriptor = artifact_storage.write_simulation( - prefix=context.artifact_prefix, - simulation=simulation, - frames=frames, - calculation_provenance=calculation_provenance, + artifact_span = ( + runtime.span( + STAGE12_SIMULATION_STAGES.name(Stage.STAGE12_SIMULATION_ARTIFACT_WRITE) + ) + if runtime is not None + else nullcontext() ) + with artifact_span: + descriptor = artifact_storage.write_simulation( + prefix=context.artifact_prefix, + simulation=simulation, + frames=frames, + calculation_provenance=calculation_provenance, + ) completed = datetime.now(UTC) persistence.replace_simulation( running.model_copy( diff --git a/projects/policyengine-simulation-executor/tests/conftest.py b/projects/policyengine-simulation-executor/tests/conftest.py index 466293e88..d87313280 100644 --- a/projects/policyengine-simulation-executor/tests/conftest.py +++ b/projects/policyengine-simulation-executor/tests/conftest.py @@ -2,6 +2,7 @@ import importlib import sys +from contextlib import nullcontext from pathlib import Path import pytest @@ -19,6 +20,36 @@ ensure_project_root_on_path() +class NoOpObservabilityRuntime: + """Minimal explicit runtime used by executor unit tests.""" + + def span(self, *args, **kwargs): + return nullcontext() + + def operation(self, *args, **kwargs): + return nullcontext() + + def set_context(self, **attributes): + return None + + def event(self, *args, **kwargs): + return None + + def record_exception(self, *args, **kwargs): + return None + + def capture_context(self): + return { + "captured_at": "2026-09-22T00:00:00Z", + "request_id": "test-request", + } + + +@pytest.fixture +def observability_runtime(): + return NoOpObservabilityRuntime() + + @pytest.fixture(autouse=True) def _forget_spm_runtime_caches(): """Keep the SPM runtime memoization out of every other test's way. diff --git a/projects/policyengine-simulation-executor/tests/test_app_redaction.py b/projects/policyengine-simulation-executor/tests/test_app_redaction.py index 15f4791e6..e70d5ab0f 100644 --- a/projects/policyengine-simulation-executor/tests/test_app_redaction.py +++ b/projects/policyengine-simulation-executor/tests/test_app_redaction.py @@ -1,4 +1,4 @@ -"""Tests for the Logfire payload redaction helper.""" +"""Tests for structured observability payload redaction.""" from __future__ import annotations @@ -6,14 +6,15 @@ def test_redact_params_strips_signed_urls_and_reform_bodies(): - """Signed URLs and the reform parameter tree must not reach Logfire.""" + """Signed URLs and reform parameter trees must not reach telemetry.""" params = { "country": "us", "scope": "macro", "data": "https://storage.googleapis.com/bucket/key?token=SECRET&expiry=123", "reform": {"gov.irs.income.bracket[2].rate": {"2024-01-01": 0.45}}, "baseline": {"gov.irs.income.bracket[2].rate": {"2023-01-01": 0.43}}, - "_telemetry": {"run_id": "run-123", "process_id": "p-1"}, + "_telemetry": {"submission_claim_id": "p-1"}, + "_observability_context": {"observability_id": "run-123"}, "_metadata": {"resolved_app_name": "policyengine-simulation-us1-500"}, } @@ -22,10 +23,8 @@ def test_redact_params_strips_signed_urls_and_reform_bodies(): # Routing context is preserved. assert redacted["country"] == "us" assert redacted["scope"] == "macro" - # Correlation id is preserved, but the rest of the telemetry envelope - # is not. - assert redacted["run_id"] == "run-123" assert "_telemetry" not in redacted + assert "_observability_context" not in redacted assert "_metadata" not in redacted # Sensitive fields are stripped entirely. @@ -47,7 +46,7 @@ def test_redact_params_strips_all_underscore_prefixed_keys(): "scope": "macro", "region_group": ["state/hi", "state/ia"], "_emit_microdata": True, - "_telemetry": {"run_id": "run-9"}, + "_observability_context": {"observability_id": "run-9"}, "_metadata": {"resolved_app_name": "x"}, } @@ -55,7 +54,6 @@ def test_redact_params_strips_all_underscore_prefixed_keys(): assert redacted["country"] == "us" assert redacted["region_group"] == ["state/hi", "state/ia"] - assert redacted["run_id"] == "run-9" # No underscore-prefixed key survives — this is what the backend forbids. assert not any(key.startswith("_") for key in redacted) diff --git a/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py b/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py index 055e2b024..cf59cfadb 100644 --- a/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py +++ b/projects/policyengine-simulation-executor/tests/test_budget_window_batch.py @@ -153,8 +153,7 @@ def _build_parent_payload(*, window_size: int = 3): scope="macro", reform={}, _telemetry={ - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, ) @@ -184,6 +183,7 @@ def _seed_parent_batch(request: BudgetWindowBatchRequest, batch_job_id: str): def test_run_budget_window_batch_impl_completes_and_respects_max_parallel( mock_batch_modal, + observability_runtime, ): request, payload = _build_parent_payload() _seed_parent_batch(request, mock_batch_modal["parent_call_id"]) @@ -230,7 +230,7 @@ def test_run_budget_window_batch_impl_completes_and_respects_max_parallel( ("policyengine-simulation-py4-10-0", "run_simulation") ] = run_simulation - result = run_budget_window_batch_impl(payload) + result = run_budget_window_batch_impl(payload, runtime=observability_runtime) state = state_module.get_batch_job_state(mock_batch_modal["parent_call_id"]) assert tracker.max_active == 2 @@ -246,7 +246,9 @@ def test_run_budget_window_batch_impl_completes_and_respects_max_parallel( assert state.result.totals.budgetaryImpact == 51 -def test_run_budget_window_batch_impl_marks_failure(mock_batch_modal): +def test_run_budget_window_batch_impl_marks_failure( + mock_batch_modal, observability_runtime +): request, payload = _build_parent_payload(window_size=2) _seed_parent_batch(request, mock_batch_modal["parent_call_id"]) @@ -272,7 +274,7 @@ def test_run_budget_window_batch_impl_marks_failure(mock_batch_modal): ("policyengine-simulation-py4-10-0", "run_simulation") ] = run_simulation - result = run_budget_window_batch_impl(payload) + result = run_budget_window_batch_impl(payload, runtime=observability_runtime) state = state_module.get_batch_job_state(mock_batch_modal["parent_call_id"]) assert result["status"] == "failed" @@ -292,7 +294,7 @@ def test_run_budget_window_batch_impl_marks_failure(mock_batch_modal): def test_scheduler_sleep_exponentially_backs_off_then_resets_on_progress( - monkeypatch, mock_batch_modal + monkeypatch, mock_batch_modal, observability_runtime ): """When every poll sees nothing resolve the runner should sleep with increasing intervals, capped at the configured max. Any progress in a @@ -339,6 +341,7 @@ def test_scheduler_sleep_exponentially_backs_off_then_resets_on_progress( bundle=PolicyEngineBundle(model_version="1.500.0"), raw_params=payload, ), + runtime=observability_runtime, poll_interval_seconds=0.5, poll_interval_max_seconds=4.0, poll_interval_backoff_factor=2.0, @@ -350,7 +353,9 @@ def test_scheduler_sleep_exponentially_backs_off_then_resets_on_progress( assert sleeps == [0.5, 1.0, 2.0, 4.0] -def test_scheduler_publishes_bounded_poll_aggregates(monkeypatch, mock_batch_modal): +def test_scheduler_publishes_bounded_poll_aggregates( + monkeypatch, mock_batch_modal, observability_runtime +): """Poll/sleep telemetry must be published as overwriting aggregate attributes, not one segment per probe: segments append nodes to the operation's in-memory segment tree, so a near-timeout batch polling for @@ -387,9 +392,9 @@ def test_scheduler_publishes_bounded_poll_aggregates(monkeypatch, mock_batch_mod monkeypatch.setattr(scheduler_module.time, "sleep", lambda _: None) attributes: dict[str, object] = {} monkeypatch.setattr( - scheduler_module, - "set_attribute", - lambda key, value: attributes.__setitem__(key, value), + observability_runtime, + "set_context", + lambda **values: attributes.update(values), ) runner = scheduler_module.BudgetWindowBatchRunner( @@ -401,6 +406,7 @@ def test_scheduler_publishes_bounded_poll_aggregates(monkeypatch, mock_batch_mod bundle=PolicyEngineBundle(model_version="1.500.0"), raw_params=payload, ), + runtime=observability_runtime, poll_interval_seconds=0.5, poll_interval_max_seconds=4.0, poll_interval_backoff_factor=2.0, @@ -418,6 +424,7 @@ def test_scheduler_publishes_bounded_poll_aggregates(monkeypatch, mock_batch_mod def test_run_budget_window_batch_impl_fails_on_malformed_child_result( mock_batch_modal, + observability_runtime, ): request, payload = _build_parent_payload(window_size=1) _seed_parent_batch(request, mock_batch_modal["parent_call_id"]) @@ -442,7 +449,7 @@ def test_run_budget_window_batch_impl_fails_on_malformed_child_result( ("policyengine-simulation-py4-10-0", "run_simulation") ] = run_simulation - result = run_budget_window_batch_impl(payload) + result = run_budget_window_batch_impl(payload, runtime=observability_runtime) state = state_module.get_batch_job_state(mock_batch_modal["parent_call_id"]) assert result["status"] == "failed" diff --git a/projects/policyengine-simulation-executor/tests/test_budget_window_context.py b/projects/policyengine-simulation-executor/tests/test_budget_window_context.py index 299ed16e4..ccf337c33 100644 --- a/projects/policyengine-simulation-executor/tests/test_budget_window_context.py +++ b/projects/policyengine-simulation-executor/tests/test_budget_window_context.py @@ -4,7 +4,10 @@ build_batch_context, build_child_simulation_request, ) -from policyengine_simulation_contract.gateway_models import BudgetWindowBatchRequest, PolicyEngineBundle +from policyengine_simulation_contract.gateway_models import ( + BudgetWindowBatchRequest, + PolicyEngineBundle, +) def _build_parent_payload(): @@ -17,8 +20,7 @@ def _build_parent_payload(): scope="macro", reform={}, _telemetry={ - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, ) @@ -47,7 +49,7 @@ def test_build_batch_context_extracts_request_and_metadata(): assert context.request.window_size == 3 assert context.request.max_parallel == 2 assert context.request.telemetry is not None - assert context.request.telemetry.run_id == "batch-run-123" + assert context.request.telemetry.submission_claim_id == "proc-123" assert context.resolved_version == "1.500.0" assert context.resolved_app_name == "policyengine-simulation-py4-10-0" assert context.bundle == PolicyEngineBundle(model_version="1.500.0") diff --git a/projects/policyengine-simulation-executor/tests/test_budget_window_scheduler.py b/projects/policyengine-simulation-executor/tests/test_budget_window_scheduler.py index 4364f86d8..62b4b4754 100644 --- a/projects/policyengine-simulation-executor/tests/test_budget_window_scheduler.py +++ b/projects/policyengine-simulation-executor/tests/test_budget_window_scheduler.py @@ -68,6 +68,7 @@ class SemiIntegrationRuntime: # instead of returning, or returns ``child_results[year]`` verbatim. child_errors: dict[str, BaseException] = field(default_factory=dict) child_results: dict[str, dict] = field(default_factory=dict) + observability: object | None = None def child_outcome_for_year(self, simulation_year: str) -> dict: return self.child_results.get( @@ -150,7 +151,10 @@ def get(self, timeout: int = 0): previous = self.runtime.current_parent_call_id self.runtime.current_parent_call_id = self.object_id try: - self._result = batch_module.run_budget_window_batch_impl(self.payload) + self._result = batch_module.run_budget_window_batch_impl( + self.payload, + runtime=self.runtime.observability, + ) finally: self.runtime.current_parent_call_id = previous return self._result @@ -185,8 +189,10 @@ def spawn(self, payload: dict): @pytest.fixture def budget_window_semi_integration_client( monkeypatch, + observability_runtime, ) -> tuple[TestClient, SemiIntegrationRuntime]: runtime = SemiIntegrationRuntime() + runtime.observability = observability_runtime runtime.dicts["simulation-api-us-versions"] = { "latest": "1.500.0", "1.500.0": "policyengine-simulation-py4-10-0", @@ -247,8 +253,7 @@ def test_budget_window_submit_and_poll_exercise_gateway_worker_seams( "window_size": 3, "max_parallel": 2, "_telemetry": { - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, }, diff --git a/projects/policyengine-simulation-executor/tests/test_canonical_spm.py b/projects/policyengine-simulation-executor/tests/test_canonical_spm.py index a285a3241..5fd9503a3 100644 --- a/projects/policyengine-simulation-executor/tests/test_canonical_spm.py +++ b/projects/policyengine-simulation-executor/tests/test_canonical_spm.py @@ -211,18 +211,20 @@ def test_result_transport_may_omit_nulls_but_not_resolved_options(): combine_spm_results([malformed], SELECTION) -def test_sync_compatibility_endpoint_preserves_explicit_null(monkeypatch): +def test_sync_compatibility_endpoint_preserves_explicit_null( + monkeypatch, observability_runtime +): import policyengine_simulation_executor.simulation as simulation captured = [] - def capture(params): + def capture(params, *, runtime): captured.append(params) raise SPMInputError("SPM_GEOGRAPHY_REQUIRED", "intentional test stop") monkeypatch.setattr(simulation, "run_simulation_impl", capture) app = FastAPI() - app.include_router(create_router()) + app.include_router(create_router(observability_runtime)) response = TestClient(app).post( "/simulate/economy/comparison", json={"country": "us", "spm": {"as_of": None}}, @@ -457,7 +459,7 @@ def test_an_unavailable_capability_is_rederived_every_time(self, monkeypatch): "SPM_SCENARIO_UNAVAILABLE", ], ) -def test_actual_http_formula_error_contract(monkeypatch, code): +def test_actual_http_formula_error_contract(monkeypatch, code, observability_runtime): error = SPMInputError(code, "Explicit input is required") assert pickle.loads(pickle.dumps(error)).to_dict() == error.to_dict() monkeypatch.setattr( @@ -465,7 +467,7 @@ def test_actual_http_formula_error_contract(monkeypatch, code): Mock(side_effect=error), ) app = FastAPI() - app.include_router(create_router()) + app.include_router(create_router(observability_runtime)) response = TestClient(app).post( "/simulate/economy/comparison", json={"country": "us", "spm": {"geography_kind": "national"}}, @@ -739,7 +741,9 @@ def test_budget_window_state_keeps_typed_failure_on_replay(): "SPM_SCENARIO_UNAVAILABLE", ], ) -def test_country_error_is_transportable_without_country_package(monkeypatch, code): +def test_country_error_is_transportable_without_country_package( + monkeypatch, code, observability_runtime +): from contextlib import nullcontext class CountryError(ValueError): @@ -754,7 +758,9 @@ def __init__(self, code, message): Mock(side_effect=CountryError(code, "Observed input required")), ) with pytest.raises(SPMInputError) as caught: - simulation_runtime.run_simulation_impl({"country": "us"}) + simulation_runtime.run_simulation_impl( + {"country": "us"}, runtime=observability_runtime + ) transported = pickle.loads(pickle.dumps(caught.value)) assert transported.to_dict() == {"code": code, "message": "Observed input required"} @@ -822,7 +828,10 @@ def test_as_of_presence_survives_budget_parent(date_fields, expected_as_of): ) assert selection["as_of"] == expected_as_of parent = _build_budget_window_parent_payload( - request, resolved_version="test", resolved_app_name="test", bundle=bundle + request, + resolved_version="test", + resolved_app_name="test", + bundle=bundle, ) assert parent["spm"] == selection diff --git a/projects/policyengine-simulation-executor/tests/test_gcp_credentials.py b/projects/policyengine-simulation-executor/tests/test_gcp_credentials.py index d618fbfd9..0bd913446 100644 --- a/projects/policyengine-simulation-executor/tests/test_gcp_credentials.py +++ b/projects/policyengine-simulation-executor/tests/test_gcp_credentials.py @@ -13,12 +13,14 @@ ) -def test_setup_gcp_credentials_deletes_temp_file_on_exit(monkeypatch): +def test_setup_gcp_credentials_deletes_temp_file_on_exit( + monkeypatch, observability_runtime +): creds = json.dumps({"type": "service_account", "project_id": "p"}) monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", creds) monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) - with setup_gcp_credentials(): + with setup_gcp_credentials(observability_runtime): path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") assert path is not None assert os.path.exists(path) @@ -31,14 +33,16 @@ def test_setup_gcp_credentials_deletes_temp_file_on_exit(monkeypatch): assert os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") is None -def test_setup_gcp_credentials_cleans_up_on_exception(monkeypatch): +def test_setup_gcp_credentials_cleans_up_on_exception( + monkeypatch, observability_runtime +): creds = json.dumps({"type": "service_account", "project_id": "q"}) monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", creds) monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) captured_path: list[str] = [] with pytest.raises(RuntimeError): - with setup_gcp_credentials(): + with setup_gcp_credentials(observability_runtime): captured_path.append(os.environ["GOOGLE_APPLICATION_CREDENTIALS"]) raise RuntimeError("boom") @@ -46,12 +50,14 @@ def test_setup_gcp_credentials_cleans_up_on_exception(monkeypatch): assert not os.path.exists(captured_path[0]) -def test_setup_gcp_credentials_preserves_existing_env(monkeypatch, tmp_path): +def test_setup_gcp_credentials_preserves_existing_env( + monkeypatch, tmp_path, observability_runtime +): existing = tmp_path / "existing.json" existing.write_text("{}") monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", str(existing)) - with setup_gcp_credentials(): + with setup_gcp_credentials(observability_runtime): assert os.environ["GOOGLE_APPLICATION_CREDENTIALS"] == str(existing) # Pre-existing var should not be disturbed. diff --git a/projects/policyengine-simulation-executor/tests/test_modal_bundle_image.py b/projects/policyengine-simulation-executor/tests/test_modal_bundle_image.py index 28959f2f7..8b84c2698 100644 --- a/projects/policyengine-simulation-executor/tests/test_modal_bundle_image.py +++ b/projects/policyengine-simulation-executor/tests/test_modal_bundle_image.py @@ -17,6 +17,8 @@ def test_modal_image_uses_policyengine_bundle_install(monkeypatch): monkeypatch.setenv("POLICYENGINE_CORE_VERSION", "3.27.1") monkeypatch.setenv("POLICYENGINE_US_VERSION", "1.700.0") monkeypatch.setenv("POLICYENGINE_UK_VERSION", "2.90.0") + monkeypatch.setenv("OBSERVABILITY_SERVICE_NAMESPACE", "policyengine.api-v1") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://collector.test") sys.modules.pop("src.modal.app", None) app = importlib.import_module("src.modal.app") @@ -52,6 +54,12 @@ def test_modal_image_uses_policyengine_bundle_install(monkeypatch): assert app.VERSION_ENV["POLICYENGINE_BUNDLE_RECEIPT"].endswith( "/.policyengine-bundle-receipt.json" ) + assert app.OBSERVABILITY_ENV["OBSERVABILITY_SERVICE_NAMESPACE"] == ( + "policyengine.api-v1" + ) + assert app.OBSERVABILITY_ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] == ( + "https://collector.test" + ) assert command_calls[0][2]["secrets"] == [app.data_secret, app.hf_secret] uv_sync_calls = [ call for call in app.simulation_image.calls if call[0] == "uv_sync" @@ -76,23 +84,22 @@ def test_modal_image_uses_policyengine_bundle_install(monkeypatch): )["dependency-groups"]["modal-simulation-image"] names = {requirement.split(">=")[0].split("[")[0] for requirement in group} assert "policyengine-observability" in names - assert "logfire" in names - # logfire needs importlib_metadata at import time on Python 3.13 but - # does not declare it; the group must keep providing it or every - # worker crashes on ``import logfire``. - assert "importlib-metadata" in names + assert "logfire" not in names # uvx drives the policyengine bundle install into the image. assert "uv" in names runtime_secret_sets = { name: kwargs["secrets"] for name, kwargs in app.app.function_calls } - for function_name in ("run_simulation", "run_budget_window_batch"): + for function_name in ( + "run_simulation", + "run_simulation_segment", + "run_budget_window_batch", + ): assert runtime_secret_sets[function_name] == [ app.gcp_secret, app.data_secret, app.hf_secret, - app.logfire_secret, ] diff --git a/projects/policyengine-simulation-executor/tests/test_national_partition.py b/projects/policyengine-simulation-executor/tests/test_national_partition.py index 27d59ced8..948564d7b 100644 --- a/projects/policyengine-simulation-executor/tests/test_national_partition.py +++ b/projects/policyengine-simulation-executor/tests/test_national_partition.py @@ -51,9 +51,7 @@ def test__partition_covers_exactly_the_registry_state_regions(self): registry = _country_module("us").model.region_registry registry_states = { - region.code - for region in registry.regions - if region.region_type == "state" + region.code for region in registry.regions if region.region_type == "state" } partition_codes = { code for group in US_NATIONAL_REGION_GROUPS for code in group diff --git a/projects/policyengine-simulation-executor/tests/test_policyengine_dependency_source.py b/projects/policyengine-simulation-executor/tests/test_policyengine_dependency_source.py index 7832250fb..6b72a30d3 100644 --- a/projects/policyengine-simulation-executor/tests/test_policyengine_dependency_source.py +++ b/projects/policyengine-simulation-executor/tests/test_policyengine_dependency_source.py @@ -93,7 +93,9 @@ def test_modal_app_name_is_keyed_to_policyengine_py_version(): def test_country_package_pins_match_policyengine_bundle(): - from policyengine_simulation_executor.release_bundle import get_country_release_bundle + from policyengine_simulation_executor.release_bundle import ( + get_country_release_bundle, + ) pyproject = _load_toml(PYPROJECT_PATH) diff --git a/projects/policyengine-simulation-executor/tests/test_segmented_national.py b/projects/policyengine-simulation-executor/tests/test_segmented_national.py index e5280a4c9..1f7012e66 100644 --- a/projects/policyengine-simulation-executor/tests/test_segmented_national.py +++ b/projects/policyengine-simulation-executor/tests/test_segmented_national.py @@ -4,6 +4,8 @@ import pytest +from conftest import NoOpObservabilityRuntime + from src.modal import segmented_national as sn from policyengine_simulation_contract.spm import SPMInputError from policyengine_simulation_executor import simulation_runtime as sr @@ -14,6 +16,7 @@ NATIONAL = {"country": "us", "scope": "macro", "time_period": "2026"} +TEST_RUNTIME = NoOpObservabilityRuntime() def _bare_country_module(): @@ -41,9 +44,7 @@ def test__non_national_shapes(self, overrides): assert sn.is_plain_national_macro({**NATIONAL, **overrides}) is False def test__knobs_do_not_change_the_shape(self): - assert ( - sn.is_plain_national_macro({**NATIONAL, "segmented": False}) is True - ) + assert sn.is_plain_national_macro({**NATIONAL, "segmented": False}) is True @pytest.mark.parametrize("region", ["us", "US", " us "]) def test__v1_style_region_us_is_national(self, region): @@ -74,28 +75,21 @@ def test__ineligible_variants(self, overrides): assert sn.should_run_segmented_national(params) is False def test__explicit_segmented_true_is_eligible(self): - assert ( - sn.should_run_segmented_national({**NATIONAL, "segmented": True}) - is True - ) + assert sn.should_run_segmented_national({**NATIONAL, "segmented": True}) is True def test__v1_style_region_us_is_eligible(self): - assert ( - sn.should_run_segmented_national({**NATIONAL, "region": "us"}) - is True - ) + assert sn.should_run_segmented_national({**NATIONAL, "region": "us"}) is True @pytest.mark.parametrize("policy_key", ["reform", "baseline"]) - def test__labor_supply_response_reforms_fall_back_monolithic( - self, policy_key - ): + def test__labor_supply_response_reforms_fall_back_monolithic(self, policy_key): # The reduce's stand-ins carry no policy, so LSR would silently # zero out (see LSR_PARAMETER_PREFIX) — these must run monolithic. params = { **NATIONAL, policy_key: { - "gov.simulation.labor_supply_responses.elasticities." - "income.all": {"2026-01-01.2100-12-31": -0.05} + "gov.simulation.labor_supply_responses.elasticities.income.all": { + "2026-01-01.2100-12-31": -0.05 + } }, } assert sn.should_run_segmented_national(params) is False @@ -124,9 +118,9 @@ def test__scopes_to_group_and_requests_microdata(self): def test__reattaches_telemetry_only_when_present(self): with_telemetry = sn.build_group_child_payload( - {**NATIONAL, "_telemetry": {"run_id": "r1"}}, ["state/ca"] + {**NATIONAL, "_telemetry": {"submission_claim_id": "p1"}}, ["state/ca"] ) - assert with_telemetry["_telemetry"] == {"run_id": "r1"} + assert with_telemetry["_telemetry"] == {"submission_claim_id": "p1"} without = sn.build_group_child_payload(dict(NATIONAL), ["state/ca"]) assert "_telemetry" not in without @@ -201,6 +195,7 @@ def _runner(fake_modal, params=None): return sn.SegmentedNationalRunner( params or dict(NATIONAL), app_name="policyengine-simulation-py-test", + runtime=TEST_RUNTIME, modal_module=fake_modal, poll_interval_seconds=0.001, poll_interval_max_seconds=0.002, @@ -244,13 +239,10 @@ def test__collects_out_of_order_completions_in_group_order( self, monkeypatch, bare_country ): calls = [ - FakeCall({"child": i}, polls_until_ready=(19 - i) % 3) - for i in range(20) + FakeCall({"child": i}, polls_until_ready=(19 - i) % 3) for i in range(20) ] runner = _runner(FakeModal(calls)) - monkeypatch.setattr( - runner, "_reduce", lambda results, country_module: results - ) + monkeypatch.setattr(runner, "_reduce", lambda results, country_module: results) assert runner.run() == [{"child": i} for i in range(20)] def test__child_failure_fails_fast_and_cancels_the_rest( @@ -259,24 +251,18 @@ def test__child_failure_fails_fast_and_cancels_the_rest( calls = _twenty_calls(polls_until_ready=5) calls[7] = FakeCall(None, error=ValueError("boom")) runner = _runner(FakeModal(calls)) - monkeypatch.setattr( - runner, "_reduce", lambda results, country_module: results - ) + monkeypatch.setattr(runner, "_reduce", lambda results, country_module: results) with pytest.raises(RuntimeError, match="Segmented national child failed"): runner.run() assert all(c.cancelled for c in calls if c.error is None) - def test__one_transient_poll_error_is_tolerated( - self, monkeypatch, bare_country - ): + def test__one_transient_poll_error_is_tolerated(self, monkeypatch, bare_country): # A single control-plane blip on one handle must not kill the job. calls = _twenty_calls() calls[3] = FakeCall({"child": 3}, errors_once=1) runner = _runner(FakeModal(calls)) - monkeypatch.setattr( - runner, "_reduce", lambda results, country_module: results - ) + monkeypatch.setattr(runner, "_reduce", lambda results, country_module: results) assert runner.run() == [{"child": i} for i in range(20)] def test__spawn_failure_cancels_already_spawned_children( @@ -285,9 +271,7 @@ def test__spawn_failure_cancels_already_spawned_children( calls = _twenty_calls(polls_until_ready=100) fake = FakeModal(calls, fail_spawn_at=15) runner = _runner(fake) - monkeypatch.setattr( - runner, "_reduce", lambda results, country_module: results - ) + monkeypatch.setattr(runner, "_reduce", lambda results, country_module: results) with pytest.raises(ConnectionError): runner.run() @@ -314,9 +298,7 @@ def test__uncovered_registry_states_ride_in_the_last_group( fake = FakeModal([FakeCall({"child": i}) for i in range(20)]) runner = _runner(fake) - monkeypatch.setattr( - runner, "_reduce", lambda results, country_module: results - ) + monkeypatch.setattr(runner, "_reduce", lambda results, country_module: results) runner.run() assert runner.groups[-1][-1] == "state/pr" @@ -334,14 +316,14 @@ def fake_build_national_output(child_results, **kwargs): captured.update(kwargs) return {"budget": {}} - monkeypatch.setattr( - sn, "build_national_output", fake_build_national_output - ) - monkeypatch.setattr( - sr, "_country_module", lambda c: _bare_country_module() - ) + monkeypatch.setattr(sn, "build_national_output", fake_build_national_output) + monkeypatch.setattr(sr, "_country_module", lambda c: _bare_country_module()) - params = {**NATIONAL, "segmented": True, "_telemetry": {"run_id": "r"}} + params = { + **NATIONAL, + "segmented": True, + "_telemetry": {"submission_claim_id": "p"}, + } runner = _runner(FakeModal(_twenty_calls()), params=params) assert runner.run() == {"budget": {}} @@ -360,20 +342,20 @@ class TestDispatchRunSimulation: def test__eligible_national_takes_the_segmented_path(self, monkeypatch): calls = {} - def fake_segmented(params, *, app_name): + def fake_segmented(params, *, app_name, runtime): calls["segmented"] = (params, app_name) return {"segmented": True} monkeypatch.setattr(sn, "run_segmented_national_impl", fake_segmented) - monkeypatch.setattr( - executor_spm, "normalize_runtime_spm", lambda params: None - ) + monkeypatch.setattr(executor_spm, "normalize_runtime_spm", lambda params: None) monkeypatch.setattr( sr, "run_simulation_impl", - lambda params: pytest.fail("monolithic path must not run"), + lambda params, **kwargs: pytest.fail("monolithic path must not run"), + ) + result = sn.dispatch_run_simulation( + dict(NATIONAL), app_name="app-x", runtime=TEST_RUNTIME ) - result = sn.dispatch_run_simulation(dict(NATIONAL), app_name="app-x") assert result == {"segmented": True} assert calls["segmented"][1] == "app-x" @@ -385,23 +367,19 @@ def fake_segmented(params, *, app_name): {"country": "uk", "scope": "macro"}, ], ) - def test__everything_else_takes_the_monolithic_path( - self, monkeypatch, params - ): + def test__everything_else_takes_the_monolithic_path(self, monkeypatch, params): monkeypatch.setattr( sn, "run_segmented_national_impl", lambda *a, **k: pytest.fail("segmented path must not run"), ) + monkeypatch.setattr(executor_spm, "normalize_runtime_spm", lambda params: None) monkeypatch.setattr( - executor_spm, "normalize_runtime_spm", lambda params: None + sr, "run_simulation_impl", lambda params, **kwargs: {"monolithic": True} ) - monkeypatch.setattr( - sr, "run_simulation_impl", lambda params: {"monolithic": True} - ) - assert sn.dispatch_run_simulation(params, app_name="app-x") == { - "monolithic": True - } + assert sn.dispatch_run_simulation( + params, app_name="app-x", runtime=TEST_RUNTIME + ) == {"monolithic": True} SPM_SELECTION = { @@ -531,14 +509,16 @@ def test__selection_is_resolved_before_the_fan_out(self, monkeypatch): ) seen = {} - def fake_segmented(params, *, app_name): + def fake_segmented(params, *, app_name, runtime): seen["params"] = params return {"segmented": True} monkeypatch.setattr(sn, "run_segmented_national_impl", fake_segmented) result = sn.dispatch_run_simulation( - {**NATIONAL, "spm": {"geography_kind": "national"}}, app_name="app-x" + {**NATIONAL, "spm": {"geography_kind": "national"}}, + app_name="app-x", + runtime=TEST_RUNTIME, ) assert result == {"segmented": True} @@ -559,9 +539,11 @@ def unavailable(params): monkeypatch.setattr( sr, "run_simulation_impl", - lambda params: pytest.fail("monolithic path must not run"), + lambda params, **kwargs: pytest.fail("monolithic path must not run"), ) with pytest.raises(SPMInputError) as error: - sn.dispatch_run_simulation(dict(NATIONAL), app_name="app-x") + sn.dispatch_run_simulation( + dict(NATIONAL), app_name="app-x", runtime=TEST_RUNTIME + ) assert error.value.code == "SPM_GEOGRAPHY_REQUIRED" diff --git a/projects/policyengine-simulation-executor/tests/test_segmented_national_reduce.py b/projects/policyengine-simulation-executor/tests/test_segmented_national_reduce.py index 806d9d243..fa424eb31 100644 --- a/projects/policyengine-simulation-executor/tests/test_segmented_national_reduce.py +++ b/projects/policyengine-simulation-executor/tests/test_segmented_national_reduce.py @@ -139,12 +139,11 @@ def __init__(self, **kwargs): assert captured["resolved_region_code"] == "us" assert captured["country"] == "us" # Both stand-ins carry the injected output_dataset. - assert captured["baseline"].output_dataset is captured["baseline"].kwargs[ - "dataset" - ] - assert captured["reform"].output_dataset is captured["reform"].kwargs[ - "dataset" - ] + assert ( + captured["baseline"].output_dataset + is captured["baseline"].kwargs["dataset"] + ) + assert captured["reform"].output_dataset is captured["reform"].kwargs["dataset"] assert captured["baseline"].kwargs["tax_benefit_model_version"] is ( country_module.model ) @@ -159,9 +158,7 @@ def test__rejects_non_us(self): year=2026, ) - def test__propagates_child_versions_over_builder_fallbacks( - self, monkeypatch - ): + def test__propagates_child_versions_over_builder_fallbacks(self, monkeypatch): # The rebuilt dataset has no artifact metadata, so the builder's # version fallbacks misreport provenance; the children loaded the # real artifact and their reported versions win. diff --git a/projects/policyengine-simulation-executor/tests/test_settings.py b/projects/policyengine-simulation-executor/tests/test_settings.py deleted file mode 100644 index f6b810622..000000000 --- a/projects/policyengine-simulation-executor/tests/test_settings.py +++ /dev/null @@ -1,58 +0,0 @@ -from policyengine_fastapi.observability import TracerCaptureMode -from policyengine_simulation_executor.settings import get_settings - - -def test_settings_default_observability_config_is_disabled(): - get_settings.cache_clear() - try: - settings = get_settings() - finally: - get_settings.cache_clear() - - config = settings.observability - - assert config.enabled is False - assert config.shadow_mode is True - assert config.service_name == "policyengine-simulation-executor" - assert config.environment == settings.environment.value - assert config.otlp_headers == {} - assert config.tracer_capture_mode == TracerCaptureMode.DISABLED - assert config.slow_run_threshold_seconds == 30.0 - - -def test_settings_expose_observability_config(monkeypatch): - monkeypatch.setenv("OBSERVABILITY_ENABLED", "true") - monkeypatch.setenv("OBSERVABILITY_SHADOW_MODE", "false") - monkeypatch.setenv("OBSERVABILITY_SERVICE_NAME", "simulation-worker") - monkeypatch.setenv("OBSERVABILITY_ENVIRONMENT", "staging") - monkeypatch.setenv("OBSERVABILITY_OTLP_ENDPOINT", "https://otlp.example") - monkeypatch.setenv( - "OBSERVABILITY_OTLP_HEADERS", - "Authorization=Bearer abc,X-Scope=ops", - ) - monkeypatch.setenv("OBSERVABILITY_ARTIFACT_BUCKET", "test-bucket") - monkeypatch.setenv("OBSERVABILITY_ARTIFACT_PREFIX", "diagnostics") - monkeypatch.setenv("OBSERVABILITY_TRACER_CAPTURE_MODE", "threshold") - monkeypatch.setenv("OBSERVABILITY_SLOW_RUN_THRESHOLD_SECONDS", "45.5") - - get_settings.cache_clear() - try: - settings = get_settings() - finally: - get_settings.cache_clear() - - config = settings.observability - - assert config.enabled is True - assert config.shadow_mode is False - assert config.service_name == "simulation-worker" - assert config.environment == "staging" - assert config.otlp_endpoint == "https://otlp.example" - assert config.otlp_headers == { - "Authorization": "Bearer abc", - "X-Scope": "ops", - } - assert config.artifact_bucket == "test-bucket" - assert config.artifact_prefix == "diagnostics" - assert config.tracer_capture_mode == TracerCaptureMode.THRESHOLD - assert config.slow_run_threshold_seconds == 45.5 diff --git a/projects/policyengine-simulation-executor/tests/test_simulation_microdata.py b/projects/policyengine-simulation-executor/tests/test_simulation_microdata.py index f9a1c6a7a..00e531814 100644 --- a/projects/policyengine-simulation-executor/tests/test_simulation_microdata.py +++ b/projects/policyengine-simulation-executor/tests/test_simulation_microdata.py @@ -51,9 +51,7 @@ def test__preserves_id_link_columns(self): def test__carries_source_dtypes_per_side_and_entity(self): out = extract_output_microdata(_fake_sim(100.0), _fake_sim(120.0)) for side in ("baseline", "reform"): - assert ( - out["dtypes"][side]["household"]["household_net_income"] == "float32" - ) + assert out["dtypes"][side]["household"]["household_net_income"] == "float32" assert "person_id" in out["dtypes"][side]["person"] diff --git a/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py b/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py index 95107cecb..8f71684b6 100644 --- a/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py +++ b/projects/policyengine-simulation-executor/tests/test_simulation_output_builder.py @@ -3,15 +3,12 @@ from __future__ import annotations import json +from contextlib import AbstractContextManager +from collections import Counter from types import SimpleNamespace import pandas as pd import pytest -from policyengine_observability import ( - ObservabilityConfig, - ObservabilityRuntime, - set_observability_runtime, -) from fixtures.test_simulation_api_contracts import ( CURRENT_SINGLE_YEAR_MACRO_KEYS, @@ -33,7 +30,7 @@ from policyengine_simulation_executor.release_bundle import ( resolve_runtime_bundle_dataset_uri, ) -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import Stage from policyengine_simulation_executor.simulation_runtime import ( DatasetSelection, RegionResolution, @@ -100,30 +97,49 @@ def ensure(self): raise AssertionError("test data is already materialized") -def _with_observability_timings(callback): - runtime = ObservabilityRuntime( - ObservabilityConfig( - service_name="policyengine-simulation-executor-test", - service_role="test", - environment="test", - otel_enabled=False, - ), - segment_registry=SegmentName, - ) - set_observability_runtime(runtime) - handle = runtime.start_operation("test_operation", flavor="unit") - try: - result = callback() - operation = handle["operation"] - return ( - result, - dict(operation.timings_ms), - dict(operation.timing_counts), - [node.as_dict() for node in operation.segment_tree], +class _TrackedSpan(AbstractContextManager): + def __init__(self, runtime, name, attributes): + self.runtime = runtime + self.node = { + "name": name, + "attrs": dict(attributes or {}), + "children": [], + } + + def __enter__(self): + target = ( + self.runtime.stack[-1]["children"] + if self.runtime.stack + else self.runtime.roots ) - finally: - runtime.end_operation(handle) - set_observability_runtime(ObservabilityRuntime.disabled()) + target.append(self.node) + self.runtime.stack.append(self.node) + self.runtime.names.append(self.node["name"]) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.runtime.stack.pop() + + +class _TrackingRuntime: + def __init__(self): + self.roots = [] + self.stack = [] + self.names = [] + self.context = [] + + def span(self, name, *, attributes=None): + return _TrackedSpan(self, name, attributes) + + def set_context(self, **attributes): + self.context.extend(attributes.items()) + + +def _with_observability_timings(callback): + runtime = _TrackingRuntime() + result = callback(runtime) + counts = Counter(runtime.names) + return result, set(runtime.names), counts, runtime.roots def _macro_baseline_reform(): @@ -517,38 +533,41 @@ def serialize(self): ) result, timings, counts, segment_tree = _with_observability_timings( - lambda: run_simulation_impl( + lambda runtime: run_simulation_impl( { "country": "us", "baseline": {"gov.test.parameter": {"2026-01-01": 1}}, "reform": {"gov.test.parameter": {"2026-01-01": 2}}, - } + }, + runtime=runtime, ) ) assert result == CURRENT_SINGLE_YEAR_MACRO_RESULT assert set(timings) >= { - SegmentName.CREDENTIAL_SETUP, - SegmentName.REQUEST_PARSE, - SegmentName.COUNTRY_MODULE_LOAD, - SegmentName.REGION_RESOLUTION, - SegmentName.DATASET_LOAD, - SegmentName.POLICY_NORMALIZATION, - SegmentName.SIMULATION_BUILD, + Stage.CREDENTIAL_SETUP, + Stage.REQUEST_PARSE, + Stage.COUNTRY_MODULE_LOAD, + Stage.REGION_RESOLUTION, + Stage.DATASET_RESOLUTION, + Stage.DATASET_LOAD, + Stage.POLICY_NORMALIZATION, + Stage.SIMULATION_BUILD, } - assert counts[SegmentName.SIMULATION_BUILD] == 2 + assert counts[Stage.SIMULATION_BUILD] == 2 assert [node["name"] for node in segment_tree] == [ - SegmentName.CREDENTIAL_SETUP, - SegmentName.REQUEST_PARSE, - SegmentName.COUNTRY_MODULE_LOAD, - SegmentName.REGION_RESOLUTION, - SegmentName.DATASET_LOAD, - SegmentName.POLICY_NORMALIZATION, - SegmentName.SIMULATION_BUILD, - SegmentName.SIMULATION_BUILD, + Stage.CREDENTIAL_SETUP, + Stage.REQUEST_PARSE, + Stage.COUNTRY_MODULE_LOAD, + Stage.REGION_RESOLUTION, + Stage.DATASET_RESOLUTION, + Stage.DATASET_LOAD, + Stage.POLICY_NORMALIZATION, + Stage.SIMULATION_BUILD, + Stage.SIMULATION_BUILD, ] simulation_builds = [ - node for node in segment_tree if node["name"] == SegmentName.SIMULATION_BUILD + node for node in segment_tree if node["name"] == Stage.SIMULATION_BUILD ] assert [node["attrs"] for node in simulation_builds] == [ {"simulation_kind": "baseline"}, @@ -613,7 +632,6 @@ def __init__(self, **kwargs): def serialize(self): return CURRENT_SINGLE_YEAR_MACRO_RESULT - attributes = [] for env in ( "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_APPLICATION_CREDENTIALS_JSON", @@ -646,25 +664,21 @@ def serialize(self): "policyengine_simulation_executor.simulation_runtime.SimulationOutputBuilder", FakeSimulationOutputBuilder, ) - monkeypatch.setattr( - "policyengine_simulation_executor.simulation_runtime.set_attribute", - lambda key, value: attributes.append((key, value)), - ) - params = { "country": "us", "baseline": {"gov.test.parameter": {"2026-01-01": 1}}, "reform": {"gov.test.parameter": {"2026-01-01": 2}}, } - run_simulation_impl(params) - assert ("baseline_artifact", "incomplete") in attributes + runtime = _TrackingRuntime() + run_simulation_impl(params, runtime=runtime) + assert ("baseline_artifact", "incomplete") in runtime.context # A plain Simulation baseline (no artifact_outcome) must emit nothing. - attributes.clear() + runtime.context.clear() build_count[0] = 0 simulations["baseline"] = object() - run_simulation_impl(params) - assert all(key != "baseline_artifact" for key, _ in attributes) + run_simulation_impl(params, runtime=runtime) + assert all(key != "baseline_artifact" for key, _ in runtime.context) def test_builder_records_output_timings_without_real_calculation(monkeypatch): @@ -799,9 +813,11 @@ def wrapper(*args, **kwargs): reform=reform, ) - result, timings, counts, segment_tree = _with_observability_timings( - builder.serialize - ) + def serialize(runtime): + builder.runtime = runtime + return builder.serialize() + + result, timings, counts, segment_tree = _with_observability_timings(serialize) assert result["model_version"] == "mock-model-version" assert result["data_version"] == "mock-data-version" @@ -822,63 +838,61 @@ def wrapper(*args, **kwargs): "uk_local_authority", } assert set(timings) >= { - SegmentName.CALCULATION, - SegmentName.SIMULATION_OUTPUT_BUILD, - SegmentName.ECONOMIC_IMPACT_ANALYSIS, - SegmentName.OUTPUT_MODEL_VERSION, - SegmentName.OUTPUT_DATA_VERSION, - SegmentName.OUTPUT_BUDGETARY_IMPACT, - SegmentName.OUTPUT_DETAILED_BUDGET, - SegmentName.OUTPUT_DECILE, - SegmentName.OUTPUT_INEQUALITY, - SegmentName.OUTPUT_POVERTY, - SegmentName.OUTPUT_INTRA_DECILE, - SegmentName.OUTPUT_WEALTH_DECILE, - SegmentName.OUTPUT_INTRA_WEALTH_DECILE, - SegmentName.OUTPUT_LABOR_SUPPLY, - SegmentName.OUTPUT_CONGRESSIONAL_DISTRICT, - SegmentName.OUTPUT_UK_CONSTITUENCY, - SegmentName.OUTPUT_UK_LOCAL_AUTHORITY, - SegmentName.OUTPUT_CLIFF, - SegmentName.RESPONSE_SERIALIZATION, - SegmentName.SIMULATION_OUTPUT_MODEL_DUMP, + Stage.CALCULATION, + Stage.SIMULATION_OUTPUT_BUILD, + Stage.ECONOMIC_IMPACT_ANALYSIS, + Stage.OUTPUT_MODEL_VERSION, + Stage.OUTPUT_DATA_VERSION, + Stage.OUTPUT_BUDGETARY_IMPACT, + Stage.OUTPUT_DETAILED_BUDGET, + Stage.OUTPUT_DECILE, + Stage.OUTPUT_INEQUALITY, + Stage.OUTPUT_POVERTY, + Stage.OUTPUT_INTRA_DECILE, + Stage.OUTPUT_WEALTH_DECILE, + Stage.OUTPUT_INTRA_WEALTH_DECILE, + Stage.OUTPUT_LABOR_SUPPLY, + Stage.OUTPUT_CONGRESSIONAL_DISTRICT, + Stage.OUTPUT_UK_CONSTITUENCY, + Stage.OUTPUT_UK_LOCAL_AUTHORITY, + Stage.OUTPUT_CLIFF, + Stage.RESPONSE_SERIALIZATION, + Stage.SIMULATION_OUTPUT_MODEL_DUMP, } - assert counts[SegmentName.ECONOMIC_IMPACT_ANALYSIS] == 1 - assert counts[SegmentName.SIMULATION_OUTPUT_MODEL_DUMP] == 1 + assert counts[Stage.ECONOMIC_IMPACT_ANALYSIS] == 1 + assert counts[Stage.SIMULATION_OUTPUT_MODEL_DUMP] == 1 assert [node["name"] for node in segment_tree] == [ - SegmentName.CALCULATION, - SegmentName.RESPONSE_SERIALIZATION, + Stage.CALCULATION, + Stage.RESPONSE_SERIALIZATION, ] calculation_children = segment_tree[0]["children"] assert [node["name"] for node in calculation_children] == [ - SegmentName.SIMULATION_OUTPUT_BUILD + Stage.SIMULATION_OUTPUT_BUILD ] output_build_children = calculation_children[0]["children"] assert {node["name"] for node in output_build_children} >= { - SegmentName.OUTPUT_BUDGETARY_IMPACT, - SegmentName.OUTPUT_DETAILED_BUDGET, - SegmentName.OUTPUT_DECILE, - SegmentName.OUTPUT_INEQUALITY, - SegmentName.OUTPUT_POVERTY, - SegmentName.OUTPUT_INTRA_DECILE, - SegmentName.OUTPUT_WEALTH_DECILE, - SegmentName.OUTPUT_INTRA_WEALTH_DECILE, - SegmentName.OUTPUT_LABOR_SUPPLY, - SegmentName.OUTPUT_CONGRESSIONAL_DISTRICT, - SegmentName.OUTPUT_UK_CONSTITUENCY, - SegmentName.OUTPUT_UK_LOCAL_AUTHORITY, - SegmentName.OUTPUT_CLIFF, - SegmentName.OUTPUT_MODEL_VERSION, - SegmentName.OUTPUT_DATA_VERSION, + Stage.OUTPUT_BUDGETARY_IMPACT, + Stage.OUTPUT_DETAILED_BUDGET, + Stage.OUTPUT_DECILE, + Stage.OUTPUT_INEQUALITY, + Stage.OUTPUT_POVERTY, + Stage.OUTPUT_INTRA_DECILE, + Stage.OUTPUT_WEALTH_DECILE, + Stage.OUTPUT_INTRA_WEALTH_DECILE, + Stage.OUTPUT_LABOR_SUPPLY, + Stage.OUTPUT_CONGRESSIONAL_DISTRICT, + Stage.OUTPUT_UK_CONSTITUENCY, + Stage.OUTPUT_UK_LOCAL_AUTHORITY, + Stage.OUTPUT_CLIFF, + Stage.OUTPUT_MODEL_VERSION, + Stage.OUTPUT_DATA_VERSION, } poverty_node = next( - node - for node in output_build_children - if node["name"] == SegmentName.OUTPUT_POVERTY + node for node in output_build_children if node["name"] == Stage.OUTPUT_POVERTY ) - assert poverty_node["children"][0]["name"] == (SegmentName.ECONOMIC_IMPACT_ANALYSIS) + assert poverty_node["children"][0]["name"] == (Stage.ECONOMIC_IMPACT_ANALYSIS) assert segment_tree[1]["children"][0]["name"] == ( - SegmentName.SIMULATION_OUTPUT_MODEL_DUMP + Stage.SIMULATION_OUTPUT_MODEL_DUMP ) @@ -1046,12 +1060,14 @@ def serialize(self): FakeSimulationOutputBuilder, ) + runtime = _TrackingRuntime() result = _run_simulation_impl_core( { "country": "us", "baseline": {"gov.test.parameter": {"2026-01-01.2100-12-31": 1}}, "reform": {"gov.test.parameter": {"2026-01-01.2100-12-31": 2}}, - } + }, + runtime=runtime, ) assert result == CURRENT_SINGLE_YEAR_MACRO_RESULT @@ -1073,6 +1089,7 @@ def serialize(self): "reform": reform_simulation, "resolved_data_version": None, "resolved_region_code": "us", + "runtime": runtime, } ] @@ -1143,7 +1160,8 @@ def serialize(self): "region": "state/ut", "baseline": {}, "reform": {}, - } + }, + runtime=_TrackingRuntime(), ) assert result == CURRENT_SINGLE_YEAR_MACRO_RESULT diff --git a/projects/policyengine-simulation-executor/tests/test_stage12_modal_app.py b/projects/policyengine-simulation-executor/tests/test_stage12_modal_app.py index 70ac61a15..9034c2d7c 100644 --- a/projects/policyengine-simulation-executor/tests/test_stage12_modal_app.py +++ b/projects/policyengine-simulation-executor/tests/test_stage12_modal_app.py @@ -5,6 +5,7 @@ import importlib import sys import tomllib +from contextlib import contextmanager from pathlib import Path import pytest @@ -12,6 +13,25 @@ from fixtures.fake_modal import install_fake_modal PROJECT_ROOT = Path(__file__).resolve().parents[1] +OBSERVABILITY_ID = "00000000-0000-4000-8000-000000000001" + + +class RecordingRuntime: + def __init__(self) -> None: + self.contexts: list[dict] = [] + self.operations: list[dict] = [] + self.shutdown_calls = 0 + + def set_context(self, **attributes) -> None: + self.contexts.append(attributes) + + @contextmanager + def operation(self, name, **kwargs): + self.operations.append({"name": name, **kwargs}) + yield + + def shutdown(self) -> None: + self.shutdown_calls += 1 def _load(monkeypatch): @@ -20,6 +40,40 @@ def _load(monkeypatch): return importlib.import_module("src.modal.v2_app") +def _observability_context() -> dict[str, str]: + return { + "captured_at": "2026-09-24T00:00:00Z", + "observability_id": OBSERVABILITY_ID, + "traceparent": "00-00000000000000000000000000000001-0000000000000001-01", + } + + +def _invoke( + monkeypatch, + module, + function_name: str, + implementation, +): + from policyengine_simulation_executor import stage12_runtime + + if function_name == "coordinate_report": + monkeypatch.setattr( + module.modal, + "current_function_call_id", + lambda: "call-1", + raising=False, + ) + monkeypatch.setattr(stage12_runtime, "coordinate_report", implementation) + return module.coordinate_report({}, {}, {}, _observability_context()) + + monkeypatch.setattr(stage12_runtime, "run_single_simulation", implementation) + return getattr(module, function_name)( + {"role": "baseline"}, + {}, + _observability_context(), + ) + + def test_v2_app_name_and_images_are_separate_and_bundle_derived(monkeypatch) -> None: module = _load(monkeypatch) @@ -38,6 +92,10 @@ def test_v2_app_name_and_images_are_separate_and_bundle_derived(monkeypatch) -> assert "--country us" not in uk_command assert module.RESOLVED_BUNDLE.bundle.policyengine_requirement in us_command assert module.gcp_secret["args"] == ("stage12-evaluation-gcp-credentials",) + assert all( + secret.get("args") != ("policyengine-logfire",) + for secret in module.worker_secrets + ) for image in ( module.us_worker_image, module.uk_worker_image, @@ -96,6 +154,57 @@ def test_v2_app_declares_validation_workers_and_non_http_coordinator( assert "asgi_app" not in vars(module) +@pytest.mark.parametrize( + "function_name", + [ + "run_single_simulation_us", + "run_single_simulation_uk", + "coordinate_report", + ], +) +def test_stage12_functions_propagate_identifier_and_close_runtime( + monkeypatch, + function_name, +) -> None: + module = _load(monkeypatch) + runtime = RecordingRuntime() + monkeypatch.setattr(module, "init_process_observability", lambda **_: runtime) + + result = _invoke(monkeypatch, module, function_name, lambda *_, **__: {"ok": True}) + + assert result == {"ok": True} + assert runtime.contexts == [{"observability_id": OBSERVABILITY_ID}] + assert runtime.operations[0]["remote_context"]["observability_id"] == ( + OBSERVABILITY_ID + ) + assert runtime.shutdown_calls == 1 + + +@pytest.mark.parametrize( + "function_name", + [ + "run_single_simulation_us", + "run_single_simulation_uk", + "coordinate_report", + ], +) +def test_stage12_functions_close_runtime_after_application_error( + monkeypatch, + function_name, +) -> None: + module = _load(monkeypatch) + runtime = RecordingRuntime() + monkeypatch.setattr(module, "init_process_observability", lambda **_: runtime) + + def fail(*_, **__): + raise LookupError("application failure") + + with pytest.raises(LookupError, match="application failure"): + _invoke(monkeypatch, module, function_name, fail) + + assert runtime.shutdown_calls == 1 + + def test_external_package_values_are_assertions_only(monkeypatch) -> None: install_fake_modal(monkeypatch) monkeypatch.setenv("STAGE12_EXPECT_US_VERSION", "not-the-bundled-version") diff --git a/projects/policyengine-simulation-executor/tests/test_standalone_simulation_contract.py b/projects/policyengine-simulation-executor/tests/test_standalone_simulation_contract.py index 58b3545ad..bba09b1bf 100644 --- a/projects/policyengine-simulation-executor/tests/test_standalone_simulation_contract.py +++ b/projects/policyengine-simulation-executor/tests/test_standalone_simulation_contract.py @@ -31,7 +31,7 @@ def test_standalone_executor_uses_its_own_service_name(): assert ( - app.state.policyengine_observability.config.service_name + app.state.policyengine_observability.config.service.name == "policyengine-simulation-executor" ) @@ -59,8 +59,9 @@ def test_standalone_simulation_openapi_keeps_legacy_schema_names(): not in spec["components"]["schemas"]["SimulationOptions"]["properties"] ) + def test_standalone_simulation_route_returns_legacy_macro_contract(monkeypatch): - def fake_run_simulation_impl(params): + def fake_run_simulation_impl(params, *, runtime): assert params == {"country": "us", "reform": {}} return CURRENT_SINGLE_YEAR_MACRO_RESULT @@ -79,7 +80,7 @@ def fake_run_simulation_impl(params): def test_standalone_simulation_route_forwards_include_cliffs(monkeypatch): - def fake_run_simulation_impl(params): + def fake_run_simulation_impl(params, *, runtime): assert params == { "country": "us", "reform": {}, diff --git a/projects/policyengine-simulation-executor/tests/test_update_v2_version_manifest.py b/projects/policyengine-simulation-executor/tests/test_update_v2_version_manifest.py index 4c795cf36..6e37359b5 100644 --- a/projects/policyengine-simulation-executor/tests/test_update_v2_version_manifest.py +++ b/projects/policyengine-simulation-executor/tests/test_update_v2_version_manifest.py @@ -63,3 +63,15 @@ def test_deployment_workflow_keeps_v2_optional_and_separate() -> None: assert update_job < v2_job v2_section = workflow[v2_job:] assert "src.modal.utils.update_version_registry" not in v2_section + deploy_section = v2_section[: v2_section.index("\n publish_stage12_v2_manifest:")] + for declaration in ( + "OBSERVABILITY_SERVICE_NAMESPACE: ${{ vars.OBSERVABILITY_SERVICE_NAMESPACE }}", + "OBSERVABILITY_TRACE_PROJECT_ID: ${{ vars.OBSERVABILITY_TRACE_PROJECT_ID }}", + "OBSERVABILITY_LOGGING_PROJECT_ID: ${{ vars.OBSERVABILITY_LOGGING_PROJECT_ID }}", + "OBSERVABILITY_LOG_NAME: ${{ vars.OBSERVABILITY_LOG_NAME }}", + "OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER }}", + "OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL: ${{ vars.OBSERVABILITY_GOOGLE_SERVICE_ACCOUNT_EMAIL }}", + "OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OBSERVABILITY_OTLP_ENDPOINT }}", + "POLICYENGINE_OTEL_GOOGLE_AUDIENCE: ${{ vars.OBSERVABILITY_OTLP_GOOGLE_AUDIENCE }}", + ): + assert declaration in deploy_section diff --git a/projects/policyengine-simulation-executor/uv.lock b/projects/policyengine-simulation-executor/uv.lock index 90f6c43dd..500d5e6df 100644 --- a/projects/policyengine-simulation-executor/uv.lock +++ b/projects/policyengine-simulation-executor/uv.lock @@ -354,18 +354,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -596,6 +584,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, ] +[[package]] +name = "google-cloud-appengine-logging" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, +] + [[package]] name = "google-cloud-core" version = "2.6.0" @@ -609,6 +626,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] +[[package]] +name = "google-cloud-logging" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, +] + [[package]] name = "google-cloud-monitoring" version = "2.30.0" @@ -695,6 +733,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, ] +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "greenlet" version = "3.4.0" @@ -711,39 +754,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, ] +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, +] + [[package]] name = "grpcio" -version = "1.80.0" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, ] [[package]] name = "grpcio-status" -version = "1.71.2" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677, upload-time = "2025-06-28T04:24:05.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -914,18 +971,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1033,24 +1078,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "logfire" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/fa/14cbd976e8ff77290c0c87f93c45029785537ffb84e2f5215416786cda11/logfire-4.6.0.tar.gz", hash = "sha256:974bc8f4efd3aa9df2d0c7b1d53b35dc4612ff5cee2be6c40dd65e5c26eab694", size = 533174, upload-time = "2025-09-10T14:56:42.168Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/9c/6384dc69601bfba19c1f5909a472b00cc4b5863f411ecf85be308beacf8f/logfire-4.6.0-py3-none-any.whl", hash = "sha256:d322844db7a2f1935976f2852a07e3753489ecc4e83fa288173b05a453bb1cbf", size = 219556, upload-time = "2025-09-10T14:56:37.278Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1356,15 +1383,14 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703, upload-time = "2025-02-04T18:17:13.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955, upload-time = "2025-02-04T18:16:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] @@ -1399,55 +1425,37 @@ wheels = [ [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d7/44098bf1ef89fc5810cdbda05faa2ae9322a0dbda4921cdc965dc68a9856/opentelemetry_exporter_otlp_proto_common-1.30.0.tar.gz", hash = "sha256:ddbfbf797e518411857d0ca062c957080279320d6235a279f7b64ced73c13897", size = 19640, upload-time = "2025-02-04T18:17:16.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/54/f4b3de49f8d7d3a78fd6e6e1a6fd27dd342eb4d82c088b9078c6a32c3808/opentelemetry_exporter_otlp_proto_common-1.30.0-py3-none-any.whl", hash = "sha256:5468007c81aa9c44dc961ab2cf368a29d3475977df83b4e30aeed42aa7bc3b38", size = 18747, upload-time = "2025-02-04T18:16:51.512Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/3e/c7246df92c25e6ce95c349ad21597b4471b01ec9471e95d5261f1629fe92/opentelemetry_exporter_otlp_proto_grpc-1.30.0.tar.gz", hash = "sha256:d0f10f0b9b9a383b7d04a144d01cb280e70362cccc613987e234183fd1f01177", size = 26256, upload-time = "2025-02-04T18:17:16.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/35/d9f63fd84c2ed8dbd407bcbb933db4ed6e1b08e7fbdaca080b9ac309b927/opentelemetry_exporter_otlp_proto_grpc-1.30.0-py3-none-any.whl", hash = "sha256:2906bcae3d80acc54fd1ffcb9e44d324e8631058b502ebe4643ca71d1ff30830", size = 18550, upload-time = "2025-02-04T18:16:52.532Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/f9/abb9191d536e6a2e2b7903f8053bf859a76bf784e3ca19a5749550ef19e4/opentelemetry_exporter_otlp_proto_http-1.30.0.tar.gz", hash = "sha256:c3ae75d4181b1e34a60662a6814d0b94dd33b628bee5588a878bed92cee6abdc", size = 15073, upload-time = "2025-02-04T18:17:18.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/3c/cdf34bc459613f2275aff9b258f35acdc4c4938dad161d17437de5d4c034/opentelemetry_exporter_otlp_proto_http-1.30.0-py3-none-any.whl", hash = "sha256:9578e790e579931c5ffd50f1e6975cbdefb6a0a0a5dea127a6ae87df10e0a589", size = 17245, upload-time = "2025-02-04T18:16:53.514Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1455,14 +1463,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5a/4c7f02235ac1269b48f3855f6be1afc641f31d4888d28b90b732fbce7141/opentelemetry_instrumentation-0.51b0.tar.gz", hash = "sha256:4ca266875e02f3988536982467f7ef8c32a38b8895490ddce9ad9604649424fa", size = 27760, upload-time = "2025-02-04T18:21:09.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/2c/48fa93f1acca9f79a06da0df7bfe916632ecc7fce1971067b3e46bcae55b/opentelemetry_instrumentation-0.51b0-py3-none-any.whl", hash = "sha256:c6de8bd26b75ec8b0e54dff59e198946e29de6a10ec65488c357d4b34aa5bdcf", size = 30923, upload-time = "2025-02-04T18:19:37.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -1471,14 +1479,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/67/8aa6e1129f641f0f3f8786e6c5d18c1f2bbe490bd4b0e91a6879e85154d2/opentelemetry_instrumentation_asgi-0.51b0.tar.gz", hash = "sha256:b3fe97c00f0bfa934371a69674981d76591c68d937b6422a5716ca21081b4148", size = 24201, upload-time = "2025-02-04T18:21:14.321Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/0a95ab37302729543631a789ba8e71dea75c520495739dbbbdfdc580b401/opentelemetry_instrumentation_asgi-0.51b0-py3-none-any.whl", hash = "sha256:e8072993db47303b633c6ec1bc74726ba4d32bd0c46c28dfadf99f79521a324c", size = 16340, upload-time = "2025-02-04T18:19:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1487,43 +1495,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/8db4422b5084177d1ef6c7855c69bf2e9e689f595a4a9b59e60588e0d427/opentelemetry_instrumentation_fastapi-0.51b0.tar.gz", hash = "sha256:1624e70f2f4d12ceb792d8a0c331244cd6723190ccee01336273b4559bc13abc", size = 19249, upload-time = "2025-02-04T18:21:28.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117, upload-time = "2025-02-04T18:20:15.267Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/d5/4a3990c461ae7e55212115e0f8f3aa412b5ce6493579e85c292245ac69ea/opentelemetry_instrumentation_httpx-0.51b0.tar.gz", hash = "sha256:061d426a04bf5215a859fea46662e5074f920e5cbde7e6ad6825a0a1b595802c", size = 17700, upload-time = "2025-02-04T18:21:31.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ba/23d4ab6402408c01f1c3f32e0c04ea6dae575bf19bcb9a0049c9e768c983/opentelemetry_instrumentation_httpx-0.51b0-py3-none-any.whl", hash = "sha256:2e3fdf755ba6ead6ab43031497c3d55d4c796d0368eccc0ce48d304b7ec6486a", size = 14109, upload-time = "2025-02-04T18:20:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, ] [[package]] name = "opentelemetry-instrumentation-logging" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/01/922f37e5d8c74bc3f92f24f3e240879ae1d04710fdeebb74f39914fc8467/opentelemetry_instrumentation_logging-0.51b0.tar.gz", hash = "sha256:2adaba90fab85a80de24728153b42ef2a74dd80fda5a3b17f3f95c11bb27c3ff", size = 9754, upload-time = "2025-02-04T18:21:33.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/e3/2d2390370b8730b6b9a2341892cd5508d0f5fa3fe30977457f89979d4085/opentelemetry_instrumentation_logging-0.51b0-py3-none-any.whl", hash = "sha256:b0855cf8b3e37ae4fdb5b77982fbfa713778762c58db3071eb3ca6c97456a049", size = 12171, upload-time = "2025-02-04T18:20:25.006Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1532,21 +1525,21 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/b2/970b1b46576b663bba64503486afe266c064c2bfd1862876420714ce29d9/opentelemetry_instrumentation_sqlalchemy-0.51b0.tar.gz", hash = "sha256:dbfe95b69006017f903dda194606be458d54789e6b3419d37161fb8861bb98a5", size = 14582, upload-time = "2025-02-04T18:21:46.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9d/72af21527ce6f286ac78dc2c75ce44b76cc45343a28f0bcbb13f213421fc/opentelemetry_instrumentation_sqlalchemy-0.65b0.tar.gz", hash = "sha256:8ec2e79f1e00808c5dc639ab5b2cfcdf9dcdef55efd6442c6019707fe2894028", size = 18007, upload-time = "2026-07-16T15:26:19.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/d4/b68c3b3388dd5107f3ed532747e112249c152ba44af71a1f96673d66e3ee/opentelemetry_instrumentation_sqlalchemy-0.51b0-py3-none-any.whl", hash = "sha256:5ff4816228b496aef1511149e2b17a25e0faacec4d5eb65bf18a9964af40f1af", size = 14132, upload-time = "2025-02-04T18:20:50.513Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/3eb3195a60b894cc1852a5657a2b64764a998085c50d7fb3452148af8f18/opentelemetry_instrumentation_sqlalchemy-0.65b0-py3-none-any.whl", hash = "sha256:4d8a2e5afc7b505a48d05cb1fb6db5f8b31681814c1d7767bd6d4b5bdd3a3047", size = 14410, upload-time = "2026-07-16T15:25:32.04Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/6e/c1ff2e3b0cd3a189a6be03fd4d63441d73d7addd9117ab5454e667b9b6c7/opentelemetry_proto-1.30.0.tar.gz", hash = "sha256:afe5c9c15e8b68d7c469596e5b32e8fc085eb9febdd6fb4e20924a93a0389179", size = 34362, upload-time = "2025-02-04T18:17:28.099Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/d7/85de6501f7216995295f7ec11e470142e6a6e080baacec1753bbf272e007/opentelemetry_proto-1.30.0-py3-none-any.whl", hash = "sha256:c6290958ff3ddacc826ca5abbeb377a31c2334387352a259ba0df37c243adc11", size = 55854, upload-time = "2025-02-04T18:17:08.024Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] @@ -1566,38 +1559,38 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633, upload-time = "2025-02-04T18:17:28.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717, upload-time = "2025-02-04T18:17:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191, upload-time = "2025-02-04T18:17:29.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416, upload-time = "2025-02-04T18:17:11.305Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/64/32510c0a803465eb6ef1f5bd514d0f5627f8abc9444ed94f7240faf6fcaa/opentelemetry_util_http-0.51b0.tar.gz", hash = "sha256:05edd19ca1cc3be3968b1e502fd94816901a365adbeaab6b6ddb974384d3a0b9", size = 8043, upload-time = "2025-02-04T18:21:59.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/dd/c371eeb9cc78abbdad231a27ce1a196a37ef96328d876ccbb381dea4c8ee/opentelemetry_util_http-0.51b0-py3-none-any.whl", hash = "sha256:0561d7a6e9c422b9ef9ae6e77eafcfcd32a2ab689f5e801475cbb67f189efa20", size = 7304, upload-time = "2025-02-04T18:21:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] [[package]] @@ -1777,10 +1770,10 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.8,<0.116.0" }, { name = "opentelemetry-exporter-gcp-monitoring", specifier = ">=1.9.0a0,<2.0.0" }, { name = "opentelemetry-exporter-gcp-trace", specifier = ">=1.9.0,<2.0.0" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-logging", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.51b0" }, - { name = "opentelemetry-sdk", specifier = ">=1.30.0,<2.0.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-sdk", specifier = ">=1.44.0,<2.0.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1794,25 +1787,26 @@ provides-extras = ["test", "build"] [[package]] name = "policyengine-observability" -version = "1.3.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2a/5e0e4c0f30b20a59ffff14364e15374f9eee494f658f41559aba97c152a2/policyengine_observability-1.3.0.tar.gz", hash = "sha256:4c2f3bb2ee59886aef6c90f1edb9c0b957ace75b1518616c0e61905b8be431e0", size = 107961, upload-time = "2026-07-01T21:10:53.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/a2/80e4bc1c4923e8f4c04483bfd22df2500b0d132c4ce1a6dec6749c94d947/policyengine_observability-1.3.0-py3-none-any.whl", hash = "sha256:7a9444e2cb8ddd7f6d54d601bd1e0a7651bd44783b78627b293a27e741f91930", size = 31087, upload-time = "2026-07-01T21:10:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-contract" @@ -1850,14 +1844,13 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "google-cloud-storage" }, - { name = "logfire" }, { name = "modal" }, { name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "policyengine" }, { name = "policyengine-core" }, { name = "policyengine-fastapi" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "policyengine-simulation-contract", extra = ["modal"] }, { name = "policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence" }, @@ -1888,10 +1881,8 @@ dev = [ modal-simulation-image = [ { name = "fastapi" }, { name = "google-cloud-storage" }, - { name = "importlib-metadata" }, - { name = "logfire" }, { name = "pip" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "psycopg", extra = ["binary"] }, { name = "pyarrow" }, { name = "spm-calculator" }, @@ -1904,15 +1895,14 @@ modal-simulation-image = [ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "google-cloud-storage", specifier = ">=2" }, - { name = "logfire", specifier = ">=3.0.0" }, { name = "modal", specifier = ">=0.73.0" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.51b0,<0.52" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0,<0.66" }, { name = "policyengine", specifier = "==5.2.0" }, { name = "policyengine-core", specifier = "==3.30.1" }, { name = "policyengine-fastapi", editable = "../../libs/policyengine-fastapi" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "policyengine-simulation-contract", extras = ["modal"], editable = "../../libs/policyengine-simulation-contract" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence", editable = "../../libs/policyengine-stage12-persistence" }, @@ -1934,10 +1924,8 @@ dev = [{ name = "policyengine-simulation-gateway", editable = "../policyengine-s modal-simulation-image = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "google-cloud-storage", specifier = ">=2" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, { name = "pip" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2,<4" }, { name = "pyarrow", specifier = ">=20,<24" }, { name = "spm-calculator", specifier = "==0.3.1" }, @@ -1953,10 +1941,9 @@ source = { editable = "../policyengine-simulation-gateway" } dependencies = [ { name = "cryptography" }, { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, { name = "modal" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "packaging" }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, { name = "pyjwt" }, ] @@ -1967,11 +1954,10 @@ requires-dist = [ { name = "cryptography", specifier = ">=41.0.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "packaging", specifier = ">=24" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, @@ -1994,9 +1980,7 @@ version = "0.1.0" source = { editable = "../../libs/policyengine-simulation-observability" } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -2005,9 +1989,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -2135,16 +2117,17 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.6" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, - { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, - { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -3134,12 +3117,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] diff --git a/projects/policyengine-simulation-gateway/pyproject.toml b/projects/policyengine-simulation-gateway/pyproject.toml index ba54553a3..806465e45 100644 --- a/projects/policyengine-simulation-gateway/pyproject.toml +++ b/projects/policyengine-simulation-gateway/pyproject.toml @@ -19,15 +19,13 @@ requires-python = ">=3.13,<3.14" dependencies = [ "fastapi>=0.115.0", "pydantic>=2.0", + # Imported by the simulation contract source copied into the Modal image. + "packaging>=24", # PyJWT powers the bearer-token decoder in gateway auth. "pyjwt>=2.10.1,<3.0.0", # JWTDecoder (policyengine-fastapi lib) needs cryptography at runtime. "cryptography>=41.0.0", - "logfire>=3.0.0", - # logfire imports importlib_metadata unconditionally on Python 3.13 - # but does not declare it (see issue #602). - "importlib-metadata>=8", - "policyengine-observability[fastapi]>=1.3.0,<2", + "policyengine-observability[fastapi,google,otlp-grpc]>=3,<4", "modal>=1.4,<2", ] diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/app.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/app.py index 8546ac0db..c15d4e867 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/app.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/app.py @@ -9,14 +9,17 @@ """ import modal +import os from pathlib import Path -from policyengine_simulation_observability.logfire_legacy import configure_logfire +from policyengine_simulation_observability.observability import ( + modal_image_environment, +) # Stable app name - this should rarely change app = modal.App("policyengine-simulation-gateway") gateway_auth_secret = modal.Secret.from_name("policyengine-gateway-auth") -logfire_secret = modal.Secret.from_name("policyengine-logfire") +OBSERVABILITY_ENV = modal_image_environment() # Lightweight image for gateway - no heavy dependencies. # @@ -52,13 +55,14 @@ def build_gateway_image() -> modal.Image: "policyengine_fastapi", copy=True, ) + .env(OBSERVABILITY_ENV) ) gateway_image = build_gateway_image() -@app.function(image=gateway_image, secrets=[gateway_auth_secret, logfire_secret]) +@app.function(image=gateway_image, secrets=[gateway_auth_secret]) @modal.asgi_app() def web_app(): """ @@ -73,7 +77,6 @@ def web_app(): from fastapi import FastAPI from policyengine_simulation_observability.observability import ( - configure_process_observability, init_simulation_observability, ) from policyengine_simulation_gateway.auth import ( @@ -81,31 +84,31 @@ def web_app(): enforce_production_auth_guard, ) from policyengine_simulation_gateway.endpoints import router + from policyengine_simulation_gateway.correlation import ( + install_observability_id_middleware, + ) api = FastAPI( title="PolicyEngine Simulation Gateway", description="Submit and poll simulation jobs. Routes to versioned simulation apps.", version="1.0.0", ) - configure_process_observability( - platform="modal", - service_role="modal_gateway", - modal_app_name="policyengine-simulation-gateway", - modal_function_name="web_app", - ) - init_simulation_observability( + runtime = init_simulation_observability( api, service_name="policyengine-simulation-gateway", service_role="modal_gateway", + platform="modal", + environment=os.getenv("MODAL_ENVIRONMENT", "local"), ) - configure_logfire("policyengine-simulation-gateway") + install_observability_id_middleware(api) + api.add_event_handler("shutdown", runtime.shutdown) # Startup guard: crash the container if GATEWAY_AUTH_DISABLED is set in # a production-equivalent Modal environment, or set without the # explicit acknowledgement env var. This prevents the bypass from # accidentally shipping to prod if a dev deploy grabs the wrong secret # bundle. See gateway.auth.enforce_production_auth_guard for the rules. - enforce_production_auth_guard() + enforce_production_auth_guard(runtime) enforce_auth_configured_guard() api.include_router(router) diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/auth.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/auth.py index a91d1aaa2..c0ddec199 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/auth.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/auth.py @@ -31,15 +31,11 @@ import logging import os -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from policyengine_observability import record_event +from policyengine_observability import ObservabilityRuntime from policyengine_fastapi.auth import JWTDecoder -from policyengine_simulation_observability.logfire_legacy import ( - legacy_logfire_attributes, - logfire_is_configured, -) logger = logging.getLogger(__name__) @@ -130,7 +126,7 @@ class AuthMisconfiguredError(RuntimeError): """Refuse to start when required auth config is absent or partial.""" -def enforce_production_auth_guard() -> None: +def enforce_production_auth_guard(runtime: ObservabilityRuntime) -> None: """Validate at startup that the auth-disabled bypass is only used in dev. Must be called from the ASGI app factory (``gateway.app.web_app``) before @@ -147,9 +143,9 @@ def enforce_production_auth_guard() -> None: the bypass impossible to set via one stray env var — an operator must actively opt in. - Even when the guard passes, emit a ``CRITICAL`` log, a structured - observability event, and a legacy Logfire event so any audit of the - service's logs surfaces the bypass immediately. + Even when the check passes, emit a ``CRITICAL`` log and a structured + observability event so any audit of the service's logs surfaces the bypass + immediately. """ if not _auth_disabled(): return @@ -180,26 +176,16 @@ def enforce_production_auth_guard() -> None: ) logger.critical(banner) try: - record_event( + runtime.event( "gateway_auth_disabled_bypass_active", - modal_environment=modal_env, - ack_value_present=True, - **legacy_logfire_attributes(), + severity="CRITICAL", + attributes={ + "modal_environment": modal_env, + "ack_value_present": True, + }, ) except Exception: # pragma: no cover - observability must never block startup pass - try: - if logfire_is_configured(): - import logfire - - logfire.error( - "gateway_auth_disabled_bypass_active", - modal_environment=modal_env, - ack_value_present=True, - **legacy_logfire_attributes(), - ) - except Exception: # pragma: no cover - logfire optional / misconfigured - pass def enforce_auth_configured_guard() -> None: @@ -235,6 +221,7 @@ def enforce_auth_configured_guard() -> None: def require_auth( + request: Request, token: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme), ) -> dict | None: """FastAPI dependency gating an endpoint behind a bearer JWT. @@ -281,4 +268,13 @@ def require_auth( detail="Gateway authentication is not configured.", ) - return decoder(token) + try: + return decoder(token) + except HTTPException: + runtime = getattr(request.app.state, "policyengine_observability", None) + if isinstance(runtime, ObservabilityRuntime): + runtime.event( + "simulation_gateway_auth_rejected", + attributes={"reason": "invalid_or_missing_token"}, + ) + raise diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/correlation.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/correlation.py new file mode 100644 index 000000000..a975d5d1a --- /dev/null +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/correlation.py @@ -0,0 +1,29 @@ +"""Canonical HTTP transport for the diagnostic correlation identifier.""" + +from __future__ import annotations + +from fastapi import FastAPI, Request +from policyengine_simulation_observability.identifiers import ( + OBSERVABILITY_ID_HEADER, + normalize_observability_id, + resolve_observability_id, +) +from starlette.datastructures import MutableHeaders + + +def install_observability_id_middleware(app: FastAPI) -> None: + """Resolve one request identifier and return it on every response.""" + + @app.middleware("http") + async def observability_id_transport(request: Request, call_next): + observability_id = resolve_observability_id( + request.headers.get(OBSERVABILITY_ID_HEADER) + ) + MutableHeaders(scope=request.scope)[OBSERVABILITY_ID_HEADER] = observability_id + request.state.observability_id = observability_id + response = await call_next(request) + response.headers[OBSERVABILITY_ID_HEADER] = ( + normalize_observability_id(response.headers.get(OBSERVABILITY_ID_HEADER)) + or observability_id + ) + return response diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/endpoints.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/endpoints.py index 63730873f..2517b60ef 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/endpoints.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/endpoints.py @@ -7,13 +7,8 @@ from typing import Optional, TypedDict import modal -from fastapi import APIRouter, Depends, HTTPException -from policyengine_observability import ( - record_error, - record_event, - segment, - set_attribute, -) +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from policyengine_observability import ObservabilityRuntime from policyengine_simulation_contract.budget_window_state import ( build_batch_status_response, @@ -31,6 +26,11 @@ resolve_spm_selection, ) from policyengine_simulation_observability.errors import log_and_redact_exception +from policyengine_simulation_observability.identifiers import ( + OBSERVABILITY_ID_HEADER, + normalize_observability_id, + resolve_observability_id, +) from policyengine_simulation_contract.gateway_models import ( BudgetWindowBatchRequest, BudgetWindowBatchStatusResponse, @@ -54,7 +54,11 @@ from policyengine_simulation_contract.hf_dataset import ( HuggingFaceDatasetReferenceError, ) -from policyengine_simulation_observability.observability import SegmentName +from policyengine_simulation_observability.stages import ( + ANNUAL_IMPACT_STAGES, + BUDGET_WINDOW_STAGES, + Stage, +) logger = logging.getLogger(__name__) @@ -90,13 +94,15 @@ def _job_metadata_store(): return modal.Dict.from_name(JOB_METADATA_DICT_NAME, create_if_missing=True) -def _record_not_found(message: str) -> None: - record_error( - LookupError(message), - handled=True, - status_code=404, - include_stack=False, - ) +def _runtime(request: Request) -> ObservabilityRuntime: + runtime = getattr(request.app.state, "policyengine_observability", None) + if not isinstance(runtime, ObservabilityRuntime): + raise RuntimeError("Simulation gateway observability is not configured") + return runtime + + +def _record_not_found(runtime: ObservabilityRuntime, message: str) -> None: + runtime.record_exception(LookupError(message), handled=True, status_code=404) def _country_bundle_data_version(country_bundle: dict) -> str | None: @@ -578,12 +584,34 @@ def _bundle_payload(bundle: PolicyEngineBundle, **dump_kwargs) -> dict: def _serialize_job_metadata( resolved_app_name: str, bundle: PolicyEngineBundle, - run_id: str | None = None, + observability_id: str | None = None, ) -> dict: return { "resolved_app_name": resolved_app_name, "policyengine_bundle": _bundle_payload(bundle), - "run_id": run_id, + "observability_id": observability_id, + } + + +def _request_observability_id(http_request: Request) -> str: + return resolve_observability_id( + getattr(http_request.state, "observability_id", None) + or http_request.headers.get(OBSERVABILITY_ID_HEADER) + ) + + +def _observability_headers(observability_id: str | None) -> dict[str, str] | None: + normalized = normalize_observability_id(observability_id) + if normalized is None: + return None + return {OBSERVABILITY_ID_HEADER: normalized} + + +def _public_job_metadata(job_metadata: dict | None) -> dict: + if not isinstance(job_metadata, dict): + return {} + return { + key: value for key, value in job_metadata.items() if key != "observability_id" } @@ -650,7 +678,11 @@ def get_app_name(country: str, version: Optional[str]) -> tuple[str, str]: response_model_exclude_none=True, dependencies=[Depends(require_auth)], ) -async def submit_simulation(request: SimulationRequest): +async def submit_simulation( + request: SimulationRequest, + http_request: Request, + response: Response, +): """ Submit a simulation job. @@ -658,36 +690,42 @@ async def submit_simulation(request: SimulationRequest): Routes to the appropriate app based on country and version params. Returns immediately with job_id for polling. """ - set_attribute("country", request.country) - set_attribute("scope", request.scope) - set_attribute("run_id", request.telemetry.run_id if request.telemetry else None) + runtime = _runtime(http_request) + observability_id = _request_observability_id(http_request) + response.headers[OBSERVABILITY_ID_HEADER] = observability_id + runtime.set_context( + country=request.country, + scope=request.scope, + observability_id=observability_id, + ) try: - with segment(SegmentName.ROUTE_RESOLUTION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.ROUTE_RESOLUTION)): route = resolve_route( request.country, request.version, request.policyengine_version, ) except ValueError as e: - record_error(e, handled=True, status_code=400, include_stack=False) + runtime.record_exception(e, handled=True, status_code=400) raise HTTPException(status_code=400, detail=str(e)) - set_attribute("resolved_app_name", route.app_name) - set_attribute("resolved_version", route.response_version) - set_attribute("policyengine_version", route.policyengine_version) + runtime.set_context( + resolved_app_name=route.app_name, + resolved_version=route.response_version, + policyengine_version=route.policyengine_version, + ) - with segment(SegmentName.REQUEST_PARSE): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.REQUEST_PARSE)): payload = request.model_dump( exclude={"version", "policyengine_version", "telemetry"}, mode="json", exclude_none=True, ) - run_id = request.telemetry.run_id if request.telemetry else None if request.telemetry is not None: payload["_telemetry"] = request.telemetry.model_dump(mode="json") try: - with segment(SegmentName.POLICYENGINE_BUNDLE): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.POLICYENGINE_BUNDLE)): bundle = _build_policyengine_bundle( request.country, route, @@ -697,36 +735,39 @@ async def submit_simulation(request: SimulationRequest): detail = spm_error_detail(exc) if detail: return failed_job_response( - error=detail.message, errors=[detail.model_dump()] + error=detail.message, + errors=[detail.model_dump()], + headers=_observability_headers(observability_id), ) - record_error(exc, handled=True, status_code=400, include_stack=False) + runtime.record_exception(exc, handled=True, status_code=400) raise HTTPException(status_code=400, detail=str(exc)) from exc if request.spm is not None: payload["spm"] = request.spm.model_dump(mode="json") + payload["_observability_context"] = runtime.capture_context() logger.info( - "Routing %s:%s to app %s (run_id=%s)", + "Routing %s:%s to app %s (observability_id=%s)", request.country, route.response_version, route.app_name, - run_id, + observability_id, ) # Spawn the job (returns immediately). ``Function.from_name`` is a lazy # handle — the control-plane RPC (hydration) happens inside ``spawn`` — # so both live under the spawn segment to time the real network cost. - with segment(SegmentName.MODAL_FUNCTION_SPAWN): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_FUNCTION_SPAWN)): sim_func = modal.Function.from_name(route.app_name, "run_simulation") call = sim_func.spawn(payload) - set_attribute("job_id", call.object_id) + runtime.set_context(job_id=call.object_id) - job_metadata = _serialize_job_metadata(route.app_name, bundle, run_id) - with segment(SegmentName.MODAL_JOB_METADATA_WRITE): + job_metadata = _serialize_job_metadata(route.app_name, bundle, observability_id) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_JOB_METADATA_WRITE)): _job_metadata_store()[call.object_id] = job_metadata - with segment(SegmentName.RESPONSE_SERIALIZATION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.RESPONSE_SERIALIZATION)): return JobSubmitResponse( job_id=call.object_id, status="submitted", @@ -735,7 +776,6 @@ async def submit_simulation(request: SimulationRequest): version=route.response_version, resolved_app_name=route.app_name, policyengine_bundle=bundle, - run_id=run_id, ) @@ -745,29 +785,40 @@ async def submit_simulation(request: SimulationRequest): response_model_exclude_none=True, dependencies=[Depends(require_auth)], ) -async def submit_budget_window_batch(request: BudgetWindowBatchRequest): +async def submit_budget_window_batch( + request: BudgetWindowBatchRequest, + http_request: Request, + response: Response, +): """ Submit a budget-window batch job. """ - set_attribute("country", request.country) - set_attribute("run_id", request.telemetry.run_id if request.telemetry else None) + runtime = _runtime(http_request) + observability_id = _request_observability_id(http_request) + response.headers[OBSERVABILITY_ID_HEADER] = observability_id + runtime.set_context( + country=request.country, + observability_id=observability_id, + ) try: - with segment(SegmentName.ROUTE_RESOLUTION): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.ROUTE_RESOLUTION)): route = resolve_route( request.country, request.version, request.policyengine_version, ) except ValueError as e: - record_error(e, handled=True, status_code=400, include_stack=False) + runtime.record_exception(e, handled=True, status_code=400) raise HTTPException(status_code=400, detail=str(e)) - set_attribute("resolved_app_name", route.app_name) - set_attribute("resolved_version", route.response_version) - set_attribute("policyengine_version", route.policyengine_version) + runtime.set_context( + resolved_app_name=route.app_name, + resolved_version=route.response_version, + policyengine_version=route.policyengine_version, + ) try: - with segment(SegmentName.POLICYENGINE_BUNDLE): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.POLICYENGINE_BUNDLE)): bundle = _build_policyengine_bundle( request.country, route, @@ -777,11 +828,13 @@ async def submit_budget_window_batch(request: BudgetWindowBatchRequest): detail = spm_error_detail(exc) if detail: return failed_job_response( - error=detail.message, errors=[detail.model_dump()] + error=detail.message, + errors=[detail.model_dump()], + headers=_observability_headers(observability_id), ) - record_error(exc, handled=True, status_code=400, include_stack=False) + runtime.record_exception(exc, handled=True, status_code=400) raise HTTPException(status_code=400, detail=str(exc)) from exc - with segment(SegmentName.REQUEST_PARSE): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.REQUEST_PARSE)): payload = _build_budget_window_parent_payload( request, resolved_version=route.response_version, @@ -790,11 +843,12 @@ async def submit_budget_window_batch(request: BudgetWindowBatchRequest): ) # Lazy handle + spawn together: the RPC cost lands in ``spawn``. - with segment(SegmentName.MODAL_FUNCTION_SPAWN): + payload["_observability_context"] = runtime.capture_context() + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.MODAL_FUNCTION_SPAWN)): batch_func = modal.Function.from_name(route.app_name, "run_budget_window_batch") call = batch_func.spawn(payload) batch_job_id = call.object_id - set_attribute("batch_job_id", batch_job_id) + runtime.set_context(batch_job_id=batch_job_id) seed_state = create_initial_batch_state( batch_job_id=batch_job_id, @@ -802,11 +856,12 @@ async def submit_budget_window_batch(request: BudgetWindowBatchRequest): resolved_version=route.response_version, resolved_app_name=route.app_name, bundle=bundle, + observability_id=observability_id, ) - with segment(SegmentName.MODAL_JOB_METADATA_WRITE): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.MODAL_JOB_METADATA_WRITE)): put_batch_job_seed(seed_state) - with segment(SegmentName.RESPONSE_SERIALIZATION): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.RESPONSE_SERIALIZATION)): return BudgetWindowBatchSubmitResponse( batch_job_id=batch_job_id, status=seed_state.status, @@ -815,7 +870,6 @@ async def submit_budget_window_batch(request: BudgetWindowBatchRequest): version=route.response_version, resolved_app_name=route.app_name, policyengine_bundle=bundle, - run_id=seed_state.run_id, ) @@ -825,7 +879,7 @@ async def submit_budget_window_batch(request: BudgetWindowBatchRequest): response_model_exclude_none=True, dependencies=[Depends(require_auth)], ) -async def get_job_status(job_id: str): +async def get_job_status(job_id: str, request: Request, response: Response): """ Poll for job status. @@ -835,51 +889,64 @@ async def get_job_status(job_id: str): - 500 with status="failed" and error on failure - 404 if job_id not found """ - set_attribute("job_id", job_id) - with segment(SegmentName.MODAL_JOB_METADATA_READ): + runtime = _runtime(request) + runtime.set_context(job_id=job_id) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_JOB_METADATA_READ)): job_metadata = _job_metadata_store().get(job_id) if job_metadata is None: - _record_not_found(f"Job not found: {job_id}") + _record_not_found(runtime, f"Job not found: {job_id}") raise HTTPException(status_code=404, detail=f"Job not found: {job_id}") + observability_id = normalize_observability_id(job_metadata.get("observability_id")) + runtime.set_context(observability_id=observability_id) + if observability_id is not None: + response.headers[OBSERVABILITY_ID_HEADER] = observability_id + public_metadata = _public_job_metadata(job_metadata) + response_headers = _observability_headers(observability_id) try: - with segment(SegmentName.MODAL_JOB_STATUS_POLL): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_JOB_STATUS_POLL)): call = modal.FunctionCall.from_id(job_id) except Exception as exc: if _is_modal_job_not_found(exc): - record_error(exc, handled=True, status_code=404, include_stack=False) + runtime.record_exception(exc, handled=True, status_code=404) raise HTTPException(status_code=404, detail=f"Job not found: {job_id}") - record_error(exc, handled=False, status_code=500) + runtime.record_exception(exc, handled=False, status_code=500) raise try: - with segment(SegmentName.MODAL_JOB_STATUS_POLL): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_JOB_STATUS_POLL)): result = call.get(timeout=0) - with segment(SegmentName.RESPONSE_SERIALIZATION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.RESPONSE_SERIALIZATION)): return JobStatusResponse( - status="complete", result=result, **(job_metadata or {}) + status="complete", result=result, **public_metadata ) except TimeoutError: - with segment(SegmentName.RESPONSE_SERIALIZATION): - return running_job_response(job_metadata) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.RESPONSE_SERIALIZATION)): + return running_job_response(public_metadata, headers=response_headers) except Exception as exc: if _is_modal_job_not_found(exc): - record_error(exc, handled=True, status_code=404, include_stack=False) + runtime.record_exception(exc, handled=True, status_code=404) raise HTTPException(status_code=404, detail=f"Job not found: {job_id}") detail = spm_error_detail(exc) if detail: return failed_job_response( error=detail.message, errors=[detail.model_dump()], - job_metadata=job_metadata, + job_metadata=public_metadata, + headers=response_headers, ) redacted = log_and_redact_exception( exc, + runtime=runtime, scope="simulation_job_status", context={"job_id": job_id}, ) - with segment(SegmentName.RESPONSE_SERIALIZATION): - return failed_job_response(error=redacted, job_metadata=job_metadata) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.RESPONSE_SERIALIZATION)): + return failed_job_response( + error=redacted, + job_metadata=public_metadata, + headers=response_headers, + ) @router.get( @@ -888,38 +955,57 @@ async def get_job_status(job_id: str): response_model_exclude_none=True, dependencies=[Depends(require_auth)], ) -async def get_budget_window_job_status(batch_job_id: str): +async def get_budget_window_job_status( + batch_job_id: str, + request: Request, + response: Response, +): """ Poll for budget-window batch status. """ - set_attribute("batch_job_id", batch_job_id) - with segment(SegmentName.BUDGET_WINDOW_STATE_LOAD): + runtime = _runtime(request) + runtime.set_context(batch_job_id=batch_job_id) + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_LOAD)): state = get_batch_job_state(batch_job_id) if state is not None: - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): - return batch_status_response(build_batch_status_response(state)) + runtime.set_context(observability_id=state.observability_id) + response_headers = _observability_headers(state.observability_id) + if response_headers is not None: + response.headers.update(response_headers) + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): + return batch_status_response( + build_batch_status_response(state), headers=response_headers + ) - with segment(SegmentName.BUDGET_WINDOW_STATE_LOAD): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_LOAD)): seed_state = get_batch_job_seed(batch_job_id) if seed_state is None: - _record_not_found(f"Budget-window job not found: {batch_job_id}") + _record_not_found(runtime, f"Budget-window job not found: {batch_job_id}") raise HTTPException( status_code=404, detail=f"Budget-window job not found: {batch_job_id}" ) + runtime.set_context(observability_id=seed_state.observability_id) + response_headers = _observability_headers(seed_state.observability_id) + if response_headers is not None: + response.headers.update(response_headers) try: - with segment(SegmentName.MODAL_JOB_STATUS_POLL): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.MODAL_JOB_STATUS_POLL)): call = modal.FunctionCall.from_id(batch_job_id) except Exception as exc: # The endpoint degrades gracefully to the seed state (a successful # 202/200 response), so this must NOT be recorded as a request - # error — record_error would emit an http_request_failed event and + # error — recording the exception would emit a request failure and # an error metric contradicting the status the client actually saw. - record_event( + runtime.event( "budget_window_parent_lookup_degraded", - batch_job_id=batch_job_id, - error_type=type(exc).__name__, - parent_call_not_found=_is_modal_job_not_found(exc), + attributes={ + "batch_job_id": batch_job_id, + "error_type": type(exc).__name__, + "parent_call_not_found": _is_modal_job_not_found(exc), + }, ) logger.warning( "Budget-window parent FunctionCall lookup failed; " @@ -927,15 +1013,23 @@ async def get_budget_window_job_status(batch_job_id: str): batch_job_id, exc_info=exc, ) - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): - return batch_status_response(build_batch_status_response(seed_state)) + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): + return batch_status_response( + build_batch_status_response(seed_state), headers=response_headers + ) try: - with segment(SegmentName.MODAL_JOB_STATUS_POLL): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.MODAL_JOB_STATUS_POLL)): result = call.get(timeout=0) except TimeoutError: - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): - return batch_status_response(build_batch_status_response(seed_state)) + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): + return batch_status_response( + build_batch_status_response(seed_state), headers=response_headers + ) except Exception as exc: # Persist the failure so subsequent polls don't resurrect the # "submitted" status from the seed store (#448). We deliberately @@ -947,6 +1041,7 @@ async def get_budget_window_job_status(batch_job_id: str): if detail else log_and_redact_exception( exc, + runtime=runtime, scope="budget_window_parent_call", context={"batch_job_id": batch_job_id}, ) @@ -954,22 +1049,29 @@ async def get_budget_window_job_status(batch_job_id: str): seed_state.status = "failed" seed_state.errors = [detail] if detail else None seed_state.error = message - with segment(SegmentName.BUDGET_WINDOW_STATE_WRITE): + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATE_WRITE)): put_batch_job_state(seed_state) put_batch_job_seed(seed_state) - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): - return batch_status_response(build_batch_status_response(seed_state)) + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): + return batch_status_response( + build_batch_status_response(seed_state), headers=response_headers + ) - with segment(SegmentName.BUDGET_WINDOW_RESULT_PARSE): - response = BudgetWindowBatchStatusResponse.model_validate(result) - with segment(SegmentName.BUDGET_WINDOW_STATUS_SERIALIZATION): - return batch_status_response(response) + with runtime.span(BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_RESULT_PARSE)): + status_response = BudgetWindowBatchStatusResponse.model_validate(result) + with runtime.span( + BUDGET_WINDOW_STAGES.name(Stage.BUDGET_WINDOW_STATUS_SERIALIZATION) + ): + return batch_status_response(status_response, headers=response_headers) @router.get("/versions", response_model=VersionsResponse) -async def list_versions() -> VersionsResponse: +async def list_versions(request: Request) -> VersionsResponse: """List all available routing versions.""" - with segment(SegmentName.ROUTE_RESOLUTION): + runtime = _runtime(request) + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.ROUTE_RESOLUTION)): state = _active_routing_state() if state: capabilities = {} @@ -989,7 +1091,7 @@ async def list_versions() -> VersionsResponse: # ``Dict.from_name`` is a lazy handle; the RPCs fire during the # ``dict(...)`` iterations, so those are what the segment must cover. - with segment(SegmentName.MODAL_DICT_READ): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_DICT_READ)): policyengine_dict = _optional_modal_dict(POLICYENGINE_VERSION_DICT_NAME) us_dict = modal.Dict.from_name("simulation-api-us-versions") uk_dict = modal.Dict.from_name("simulation-api-uk-versions") @@ -1011,14 +1113,15 @@ def _version_map_from_state(state: VersionRoutingState, kind: str) -> VersionMap @router.get("/versions/{kind}", response_model=VersionMap) -async def get_country_versions(kind: str) -> VersionMap: +async def get_country_versions(kind: str, request: Request) -> VersionMap: """Get available versions for policyengine, US, or UK routing.""" + runtime = _runtime(request) kind_lower = kind.lower() if kind_lower not in SUPPORTED_ROUTE_KINDS: - _record_not_found(f"Unknown version kind: {kind}") + _record_not_found(runtime, f"Unknown version kind: {kind}") raise HTTPException(status_code=404, detail=f"Unknown version kind: {kind}") - with segment(SegmentName.ROUTE_RESOLUTION): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.ROUTE_RESOLUTION)): state = _active_routing_state() if state: return _version_map_from_state(state, kind_lower) @@ -1026,11 +1129,11 @@ async def get_country_versions(kind: str) -> VersionMap: # The ``dict(...)`` iteration is where the Modal Dict RPCs happen; the # from_name handle itself is lazy and free. if kind_lower == "policyengine": - with segment(SegmentName.MODAL_DICT_READ): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_DICT_READ)): version_dict = _optional_modal_dict(POLICYENGINE_VERSION_DICT_NAME) return VersionMap(dict(version_dict) if version_dict is not None else {}) - with segment(SegmentName.MODAL_DICT_READ): + with runtime.span(ANNUAL_IMPACT_STAGES.name(Stage.MODAL_DICT_READ)): version_dict = modal.Dict.from_name(f"simulation-api-{kind_lower}-versions") return VersionMap(dict(version_dict)) diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/responses.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/responses.py index 562d42b94..9b653fc7e 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/responses.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/responses.py @@ -9,13 +9,6 @@ ) -class AcceptedResponse(JSONResponse): - """Shared 202 JSON response.""" - - def __init__(self, content: dict): - super().__init__(status_code=202, content=content) - - class ServerErrorResponse(JSONResponse): """Shared 500 JSON response.""" @@ -38,30 +31,46 @@ def batch_status_payload(response: BudgetWindowBatchStatusResponse) -> dict: return payload -def batch_status_response(response: BudgetWindowBatchStatusResponse): +def batch_status_response( + response: BudgetWindowBatchStatusResponse, + *, + headers: dict[str, str] | None = None, +): payload = batch_status_payload(response) if response.status in {"submitted", "running"}: - return AcceptedResponse(payload) + return JSONResponse(status_code=202, content=payload, headers=headers) if response.status == "failed": return JSONResponse( - status_code=400 if response.errors else 500, content=payload + status_code=400 if response.errors else 500, + content=payload, + headers=headers, ) - return response + return JSONResponse(status_code=200, content=payload, headers=headers) -def running_job_response(job_metadata: dict | None = None) -> AcceptedResponse: - return AcceptedResponse( - { +def running_job_response( + job_metadata: dict | None = None, + *, + headers: dict[str, str] | None = None, +) -> JSONResponse: + return JSONResponse( + status_code=202, + content={ "status": "running", "result": None, "error": None, **(job_metadata or {}), - } + }, + headers=headers, ) def failed_job_response( - *, error: str, job_metadata: dict | None = None, errors: list[dict] | None = None + *, + error: str, + job_metadata: dict | None = None, + errors: list[dict] | None = None, + headers: dict[str, str] | None = None, ) -> JSONResponse: return JSONResponse( status_code=400 if errors else 500, @@ -72,4 +81,5 @@ def failed_job_response( "error": error, **(job_metadata or {}), }, + headers=headers, ) diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/smoke_app.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/smoke_app.py index e56ede89c..b12c16102 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/smoke_app.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/smoke_app.py @@ -1,8 +1,7 @@ """Pre-merge image smoke: import the gateway inside its real image. -Runs the true entrypoint surface — every package module, the explicit -``import logfire`` (issue #602's crash path), and the real ASGI factory — -inside the exact image the deploy ships. Catches in-image breakage +Runs the true entrypoint surface — every package module and the real ASGI +factory — inside the exact image the deploy ships. Catches in-image breakage (missing lock packages, mount gaps, import-time crashes) before merge instead of at the post-merge beta integration tests. @@ -27,10 +26,6 @@ def smoke_import_gateway() -> dict: import os import pkgutil - # The #602 crash path: logfire imports importlib_metadata at import - # time; the app only touches logfire lazily, so import it explicitly. - import logfire # noqa: F401 - import policyengine_simulation_contract import policyengine_simulation_gateway import policyengine_simulation_observability diff --git a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/testing.py b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/testing.py index 71b520028..8648a89b1 100644 --- a/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/testing.py +++ b/projects/policyengine-simulation-gateway/src/policyengine_simulation_gateway/testing.py @@ -11,6 +11,9 @@ init_simulation_observability, ) from policyengine_simulation_gateway.auth import require_auth +from policyengine_simulation_gateway.correlation import ( + install_observability_id_middleware, +) from policyengine_simulation_gateway.endpoints import router @@ -27,11 +30,15 @@ def create_gateway_app(*, authenticate: bool = True) -> FastAPI: description="Test instance for unit tests", version="0.0.1", ) - init_simulation_observability( + runtime = init_simulation_observability( app, service_name="policyengine-simulation-gateway", service_role="modal_gateway", + platform="local", + environment="test", ) + install_observability_id_middleware(app) + app.add_event_handler("shutdown", runtime.shutdown) app.include_router(router) if authenticate: app.dependency_overrides[require_auth] = lambda: None diff --git a/projects/policyengine-simulation-gateway/tests/golden/openapi.json b/projects/policyengine-simulation-gateway/tests/golden/openapi.json index 45acf4564..bddec43f3 100644 --- a/projects/policyengine-simulation-gateway/tests/golden/openapi.json +++ b/projects/policyengine-simulation-gateway/tests/golden/openapi.json @@ -763,17 +763,6 @@ "type": "null" } ] - }, - "run_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Run Id" } }, "type": "object", @@ -812,17 +801,6 @@ }, "policyengine_bundle": { "$ref": "#/components/schemas/PolicyEngineBundle" - }, - "run_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Run Id" } }, "type": "object", @@ -1343,17 +1321,6 @@ "type": "null" } ] - }, - "run_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Run Id" } }, "type": "object", @@ -1392,17 +1359,6 @@ }, "policyengine_bundle": { "$ref": "#/components/schemas/PolicyEngineBundle" - }, - "run_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Run Id" } }, "type": "object", @@ -2314,11 +2270,7 @@ }, "TelemetryEnvelope": { "properties": { - "run_id": { - "type": "string", - "title": "Run Id" - }, - "process_id": { + "submission_claim_id": { "anyOf": [ { "type": "string" @@ -2327,29 +2279,7 @@ "type": "null" } ], - "title": "Process Id" - }, - "request_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Request Id" - }, - "traceparent": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Traceparent" + "title": "Submission Claim Id" }, "requested_at": { "anyOf": [ @@ -2422,11 +2352,8 @@ }, "additionalProperties": false, "type": "object", - "required": [ - "run_id" - ], "title": "TelemetryEnvelope", - "description": "Minimal shared telemetry payload shape for gateway and worker code." + "description": "Bounded internal metadata passed from API request to Modal worker." }, "ValidationError": { "properties": { diff --git a/projects/policyengine-simulation-gateway/tests/test_auth.py b/projects/policyengine-simulation-gateway/tests/test_auth.py index f4db190c7..3639823b2 100644 --- a/projects/policyengine-simulation-gateway/tests/test_auth.py +++ b/projects/policyengine-simulation-gateway/tests/test_auth.py @@ -4,6 +4,8 @@ import pytest from fastapi.testclient import TestClient +from types import SimpleNamespace +from unittest.mock import Mock from policyengine_simulation_gateway.testing import create_gateway_app from policyengine_simulation_gateway import auth as auth_module @@ -32,6 +34,12 @@ ] +def _request(): + return SimpleNamespace( + app=SimpleNamespace(state=SimpleNamespace(policyengine_observability=Mock())) + ) + + @pytest.fixture def unauthenticated_client(monkeypatch) -> TestClient: """A client where the real auth dependency is active but no token is @@ -71,7 +79,7 @@ def test__given_no_bearer_token__then_gated_endpoint_returns_403( def test__given_auth_disabled_env__then_dependency_returns_none(monkeypatch): monkeypatch.setenv(auth_module.GATEWAY_AUTH_DISABLED_ENV, "1") - assert auth_module.require_auth(token=None) is None + assert auth_module.require_auth(_request(), token=None) is None def test__given_auth_not_configured_and_not_required__then_dependency_allows( @@ -82,7 +90,7 @@ def test__given_auth_not_configured_and_not_required__then_dependency_allows( monkeypatch.delenv(auth_module.GATEWAY_AUTH_ISSUER_ENV, raising=False) monkeypatch.delenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, raising=False) - assert auth_module.require_auth(token=None) is None + assert auth_module.require_auth(_request(), token=None) is None def test__given_auth_configured_but_not_required__then_dependency_allows( @@ -93,7 +101,7 @@ def test__given_auth_configured_but_not_required__then_dependency_allows( monkeypatch.setenv(auth_module.GATEWAY_AUTH_ISSUER_ENV, "https://issuer.example/") monkeypatch.setenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, "aud") - assert auth_module.require_auth(token=None) is None + assert auth_module.require_auth(_request(), token=None) is None def test__given_auth_required_and_misconfigured__then_dependency_raises_503( @@ -107,7 +115,7 @@ def test__given_auth_required_and_misconfigured__then_dependency_raises_503( monkeypatch.delenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, raising=False) with pytest.raises(HTTPException) as exc_info: - auth_module.require_auth(token=None) + auth_module.require_auth(_request(), token=None) assert exc_info.value.status_code == 503 @@ -121,7 +129,7 @@ def test__given_partial_auth_config__then_dependency_raises_503(monkeypatch): monkeypatch.delenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, raising=False) with pytest.raises(HTTPException) as exc_info: - auth_module.require_auth(token=None) + auth_module.require_auth(_request(), token=None) assert exc_info.value.status_code == 503 @@ -243,7 +251,7 @@ def test__given_auth_enabled__then_guard_noops(self, monkeypatch): monkeypatch.delenv(auth_module.MODAL_ENVIRONMENT_ENV, raising=False) # Must not raise even without any prod env configured. - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) def test__given_disabled_and_modal_env_missing__then_refuses(self, monkeypatch): """Unset MODAL_ENVIRONMENT is treated as production: refuse.""" @@ -255,7 +263,7 @@ def test__given_disabled_and_modal_env_missing__then_refuses(self, monkeypatch): ) with pytest.raises(auth_module.AuthDisabledInProductionError): - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) @pytest.mark.parametrize("prod_env", ["main", "prod", "production", "PROD"]) def test__given_disabled_and_prod_modal_env__then_refuses( @@ -270,7 +278,7 @@ def test__given_disabled_and_prod_modal_env__then_refuses( ) with pytest.raises(auth_module.AuthDisabledInProductionError): - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) def test__given_disabled_in_dev_without_ack__then_refuses(self, monkeypatch): """A single env var (``GATEWAY_AUTH_DISABLED=1``) is not enough.""" @@ -279,7 +287,7 @@ def test__given_disabled_in_dev_without_ack__then_refuses(self, monkeypatch): monkeypatch.delenv(auth_module.GATEWAY_AUTH_DISABLED_ACK_ENV, raising=False) with pytest.raises(auth_module.AuthDisabledWithoutAckError): - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) def test__given_disabled_in_dev_with_wrong_ack__then_refuses(self, monkeypatch): """ACK must exactly match the magic string — truthy is not enough.""" @@ -288,7 +296,7 @@ def test__given_disabled_in_dev_with_wrong_ack__then_refuses(self, monkeypatch): monkeypatch.setenv(auth_module.GATEWAY_AUTH_DISABLED_ACK_ENV, "yes") with pytest.raises(auth_module.AuthDisabledWithoutAckError): - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) def test__given_disabled_in_dev_with_correct_ack__then_allows_and_logs( self, monkeypatch, caplog @@ -304,20 +312,13 @@ def test__given_disabled_in_dev_with_correct_ack__then_allows_and_logs( ) with caplog.at_level(logging.CRITICAL, logger=auth_module.logger.name): - auth_module.enforce_production_auth_guard() + auth_module.enforce_production_auth_guard(Mock()) assert any( "GATEWAY AUTH IS DISABLED" in record.message for record in caplog.records ), f"Expected critical auth-disabled banner, got {caplog.records!r}" - def test__given_bypass_active__then_logfire_event_gated_on_configuration( - self, monkeypatch - ): - """The legacy Logfire audit event fires only when configure_logfire - actually ran with a token — not based on logfire's send_to_logfire - flag, which is True even on an unconfigured instance.""" - import sys - + def test__given_bypass_active__then_runtime_event_is_recorded(self, monkeypatch): monkeypatch.setenv(auth_module.GATEWAY_AUTH_DISABLED_ENV, "1") monkeypatch.setenv(auth_module.MODAL_ENVIRONMENT_ENV, "dev") monkeypatch.setenv( @@ -325,26 +326,16 @@ def test__given_bypass_active__then_logfire_event_gated_on_configuration( auth_module.GATEWAY_AUTH_DISABLED_ACK_VALUE, ) - class _FakeLogfire: - def __init__(self): - self.calls = [] - - def error(self, *args, **kwargs): - self.calls.append((args, kwargs)) - - fake_logfire = _FakeLogfire() - monkeypatch.setitem(sys.modules, "logfire", fake_logfire) - - monkeypatch.setattr(auth_module, "logfire_is_configured", lambda: False) - auth_module.enforce_production_auth_guard() - assert fake_logfire.calls == [] - - monkeypatch.setattr(auth_module, "logfire_is_configured", lambda: True) - auth_module.enforce_production_auth_guard() - assert len(fake_logfire.calls) == 1 - args, kwargs = fake_logfire.calls[0] - assert args == ("gateway_auth_disabled_bypass_active",) - assert kwargs["modal_environment"] == "dev" + runtime = Mock() + auth_module.enforce_production_auth_guard(runtime) + runtime.event.assert_called_once_with( + "gateway_auth_disabled_bypass_active", + severity="CRITICAL", + attributes={ + "modal_environment": "dev", + "ack_value_present": True, + }, + ) class TestAuthConfiguredGuard: diff --git a/projects/policyengine-simulation-gateway/tests/test_endpoints.py b/projects/policyengine-simulation-gateway/tests/test_endpoints.py index 5483f02db..ce63c2307 100644 --- a/projects/policyengine-simulation-gateway/tests/test_endpoints.py +++ b/projects/policyengine-simulation-gateway/tests/test_endpoints.py @@ -6,16 +6,19 @@ """ from copy import deepcopy +from uuid import UUID import pytest from fastapi.testclient import TestClient - from fixtures.gateway_endpoints import ( TEST_APP_RELEASE_BUNDLE, TEST_ROUTING_STATE, resolve_test_dataset_uri, ) from policyengine_simulation_contract.hf_dataset import HuggingFaceDatasetReferenceError +from policyengine_simulation_observability.identifiers import OBSERVABILITY_ID_HEADER + +OBSERVABILITY_ID = "00000000-0000-4000-8000-000000000001" def expected_bundle( @@ -285,6 +288,15 @@ def test__given_submission__then_returns_job_id_and_poll_url( assert data["job_id"] == "mock-job-id-123" assert data["poll_url"] == "/jobs/mock-job-id-123" assert data["status"] == "submitted" + assert "observability_id" not in data + generated_observability_id = response.headers[OBSERVABILITY_ID_HEADER] + assert str(UUID(generated_observability_id)) == generated_observability_id + assert ( + mock_modal["func"].last_payload["_observability_context"][ + "observability_id" + ] + == generated_observability_id + ) def test__given_submission_with_include_cliffs__then_forwards_worker_flag( self, mock_modal, client: TestClient @@ -307,13 +319,69 @@ def test__given_submission_with_include_cliffs__then_forwards_worker_flag( assert response.status_code == 200 assert mock_modal["func"].last_payload["include_cliffs"] is True - def test__given_submission_with_telemetry__then_preserves_run_id( + @pytest.mark.parametrize( + "field", + ["observability_id", "run_id", "request_id", "traceparent"], + ) + def test__given_identity_in_body_telemetry__then_uses_only_header( + self, field: str, mock_modal, client: TestClient + ): + mock_modal["dicts"]["simulation-api-us-versions"] = { + "latest": "1.500.0", + "1.500.0": "policyengine-simulation-py4-10-0", + } + response = client.post( + "/simulate/economy/comparison", + json={ + "country": "us", + "scope": "macro", + "reform": {}, + "_telemetry": { + field: "identifier", + "submission_claim_id": "claim-123", + }, + }, + headers={OBSERVABILITY_ID_HEADER: OBSERVABILITY_ID}, + ) + + assert response.status_code == 200 + assert response.headers[OBSERVABILITY_ID_HEADER] == OBSERVABILITY_ID + spawned = mock_modal["func"].last_payload + assert field not in spawned["_telemetry"] + assert spawned["_telemetry"]["submission_claim_id"] == "claim-123" + assert spawned["_observability_context"]["observability_id"] == OBSERVABILITY_ID + + def test__given_legacy_process_id__then_maps_submission_claim_id( + self, mock_modal, client: TestClient + ): + mock_modal["dicts"]["simulation-api-us-versions"] = { + "latest": "1.500.0", + "1.500.0": "policyengine-simulation-py4-10-0", + } + response = client.post( + "/simulate/economy/comparison", + json={ + "country": "us", + "scope": "macro", + "reform": {}, + "_telemetry": {"process_id": "legacy-claim-123"}, + }, + headers={OBSERVABILITY_ID_HEADER: OBSERVABILITY_ID}, + ) + + assert response.status_code == 200 + assert ( + mock_modal["func"].last_payload["_telemetry"]["submission_claim_id"] + == "legacy-claim-123" + ) + + def test__given_submission_header__then_propagates_observability_id( self, mock_modal, client: TestClient ): """ - Given a simulation submission with internal telemetry metadata + Given a simulation submission with the diagnostic identifier header When the request completes - Then the spawned payload preserves telemetry and the response echoes run_id. + Then the Modal context and response header preserve the identifier. """ mock_modal["dicts"]["simulation-api-us-versions"] = { "latest": "1.500.0", @@ -325,18 +393,28 @@ def test__given_submission_with_telemetry__then_preserves_run_id( "scope": "macro", "reform": {}, "_telemetry": { - "run_id": "run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, } - response = client.post("/simulate/economy/comparison", json=request_body) + response = client.post( + "/simulate/economy/comparison", + json=request_body, + headers={OBSERVABILITY_ID_HEADER: OBSERVABILITY_ID}, + ) assert response.status_code == 200 data = response.json() - assert data["run_id"] == "run-123" - assert mock_modal["func"].last_payload["_telemetry"]["run_id"] == "run-123" + assert "observability_id" not in data + assert response.headers[OBSERVABILITY_ID_HEADER] == OBSERVABILITY_ID + assert ( + mock_modal["func"].last_payload["_observability_context"][ + "observability_id" + ] + == OBSERVABILITY_ID + ) + assert "observability_id" not in mock_modal["func"].last_payload["_telemetry"] def test__given_submission_without_data__then_returns_default_bundle_metadata( self, mock_modal, client: TestClient @@ -751,7 +829,7 @@ def test__given_submission_with_runtime_bundle__then_rejects_revision_override( "model_version": "1.500.0", "data_version": "custom-v2", }, - "_metadata": {"process_id": "process-123"}, + "_metadata": {"submission_claim_id": "process-123"}, } response = client.post("/simulate/economy/comparison", json=request_body) @@ -811,11 +889,15 @@ def test__given_submitted_job__then_job_status_includes_bundle_metadata( assert response.status_code == 200 data = response.json() assert data["status"] == "complete" - assert "run_id" not in data + assert "observability_id" not in data + assert ( + response.headers[OBSERVABILITY_ID_HEADER] + == (submit_response.headers[OBSERVABILITY_ID_HEADER]) + ) assert data["resolved_app_name"] == "policyengine-simulation-py4-10-0" assert data["policyengine_bundle"] == expected_bundle("us", "1.500.0") - def test__given_submitted_job_with_telemetry__then_polling_echoes_run_id( + def test__given_submitted_job_with_header__then_polling_echoes_header( self, mock_modal, client: TestClient ): mock_modal["dicts"]["simulation-api-us-versions"] = { @@ -830,17 +912,18 @@ def test__given_submitted_job_with_telemetry__then_polling_echoes_run_id( "scope": "macro", "reform": {}, "_telemetry": { - "run_id": "run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, }, + headers={OBSERVABILITY_ID_HEADER: OBSERVABILITY_ID}, ) response = client.get(f"/jobs/{submit_response.json()['job_id']}") assert response.status_code == 200 - assert response.json()["run_id"] == "run-123" + assert "observability_id" not in response.json() + assert response.headers[OBSERVABILITY_ID_HEADER] == OBSERVABILITY_ID def test__given_unknown_job_id__then_polling_returns_404( self, mock_modal, client: TestClient, monkeypatch @@ -850,12 +933,10 @@ def test__given_unknown_job_id__then_polling_returns_404( When polling job status Then the gateway returns 404 before asking Modal for a call result. """ - from policyengine_simulation_gateway import endpoints as endpoints_module - recorded_errors = [] monkeypatch.setattr( - endpoints_module, - "record_error", + client.app.state.policyengine_observability, + "record_exception", lambda exc, **kwargs: recorded_errors.append((exc, kwargs)), ) @@ -867,7 +948,6 @@ def test__given_unknown_job_id__then_polling_returns_404( assert recorded_errors[0][1] == { "handled": True, "status_code": 404, - "include_stack": False, } def test__given_lazy_modal_call_without_metadata__then_polling_returns_404( @@ -1019,12 +1099,10 @@ def test__given_active_state__then_policyengine_versions_endpoint_works( def test__given_unknown_version_kind__then_records_handled_404( self, mock_modal, client: TestClient, monkeypatch ): - from policyengine_simulation_gateway import endpoints as endpoints_module - recorded_errors = [] monkeypatch.setattr( - endpoints_module, - "record_error", + client.app.state.policyengine_observability, + "record_exception", lambda exc, **kwargs: recorded_errors.append((exc, kwargs)), ) @@ -1036,7 +1114,6 @@ def test__given_unknown_version_kind__then_records_handled_404( assert recorded_errors[0][1] == { "handled": True, "status_code": 404, - "include_stack": False, } def test__given_no_active_state__then_versions_fall_back_to_old_dicts( @@ -1188,7 +1265,11 @@ def test__given_budget_window_submission__then_returns_parent_batch_job_id( "policyengine-simulation-py4-10-0", "run_budget_window_batch", ) - assert response.json() == { + data = response.json() + assert "observability_id" not in data + generated_observability_id = response.headers[OBSERVABILITY_ID_HEADER] + assert str(UUID(generated_observability_id)) == generated_observability_id + assert data == { "batch_job_id": "mock-batch-job-id-123", "status": "submitted", "poll_url": "/budget-window-jobs/mock-batch-job-id-123", @@ -1201,12 +1282,10 @@ def test__given_budget_window_submission__then_returns_parent_batch_job_id( def test__given_unknown_budget_window_job__then_records_handled_404( self, mock_modal, client: TestClient, monkeypatch ): - from policyengine_simulation_gateway import endpoints as endpoints_module - recorded_errors = [] monkeypatch.setattr( - endpoints_module, - "record_error", + client.app.state.policyengine_observability, + "record_exception", lambda exc, **kwargs: recorded_errors.append((exc, kwargs)), ) @@ -1222,7 +1301,6 @@ def test__given_unknown_budget_window_job__then_records_handled_404( assert recorded_errors[0][1] == { "handled": True, "status_code": 404, - "include_stack": False, } def test__given_parent_lookup_failure__then_degraded_poll_is_not_an_error( @@ -1236,19 +1314,20 @@ def test__given_parent_lookup_failure__then_degraded_poll_is_not_an_error( error metrics contradicting the successful response. """ from fixtures.gateway_endpoints import MockFunctionCall - from policyengine_simulation_gateway import endpoints as endpoints_module recorded_errors = [] recorded_events = [] monkeypatch.setattr( - endpoints_module, - "record_error", + client.app.state.policyengine_observability, + "record_exception", lambda exc, **kwargs: recorded_errors.append((exc, kwargs)), ) monkeypatch.setattr( - endpoints_module, - "record_event", - lambda event, **kwargs: recorded_events.append((event, kwargs)), + client.app.state.policyengine_observability, + "event", + lambda event, **kwargs: recorded_events.append( + (event, kwargs["attributes"]) + ), ) mock_modal["dicts"]["simulation-api-us-versions"] = { @@ -1331,11 +1410,11 @@ def test__given_budget_window_submission__then_initial_poll_returns_seed_state( "window_size": 3, "max_parallel": 2, "_telemetry": { - "run_id": "batch-run-123", - "process_id": "proc-123", + "submission_claim_id": "proc-123", "capture_mode": "disabled", }, }, + headers={OBSERVABILITY_ID_HEADER: OBSERVABILITY_ID}, ) response = client.get( @@ -1355,8 +1434,8 @@ def test__given_budget_window_submission__then_initial_poll_returns_seed_state( "error": None, "resolved_app_name": "policyengine-simulation-py4-10-0", "policyengine_bundle": expected_bundle("us", "1.500.0"), - "run_id": "batch-run-123", } + assert response.headers[OBSERVABILITY_ID_HEADER] == OBSERVABILITY_ID def test__given_batch_state__then_poll_returns_completed_response( self, mock_modal, client: TestClient @@ -1426,7 +1505,7 @@ def test__given_batch_state__then_poll_returns_completed_response( error=None, created_at="2026-01-01T00:00:00+00:00", updated_at="2026-01-01T00:00:01+00:00", - run_id="batch-run-123", + observability_id=OBSERVABILITY_ID, ) ) @@ -1435,7 +1514,8 @@ def test__given_batch_state__then_poll_returns_completed_response( assert response.status_code == 200 assert response.json()["status"] == "complete" assert response.json()["result"]["totals"]["budgetaryImpact"] == 32 - assert response.json()["run_id"] == "batch-run-123" + assert "observability_id" not in response.json() + assert response.headers[OBSERVABILITY_ID_HEADER] == OBSERVABILITY_ID def test__given_non_integer_start_year__then_budget_window_submit_returns_422( self, mock_modal, client: TestClient diff --git a/projects/policyengine-simulation-gateway/tests/test_import_coverage.py b/projects/policyengine-simulation-gateway/tests/test_import_coverage.py index 331f4e944..d0854d948 100644 --- a/projects/policyengine-simulation-gateway/tests/test_import_coverage.py +++ b/projects/policyengine-simulation-gateway/tests/test_import_coverage.py @@ -1,15 +1,9 @@ -"""Import-coverage tests: parity between the test env and the image env. - -CI runs this suite in the environment resolved from this project's -uv.lock — the same lock the Modal image installs with uv_sync(frozen=True). -Importing every module and building the real ASGI app here means a -package missing from the lock fails unit tests deterministically instead -of crashing the deployed container (issue #602). -""" +"""Import coverage for the packages installed in the gateway image.""" import importlib import pkgutil +import policyengine_observability import policyengine_simulation_gateway from policyengine_simulation_gateway.testing import create_gateway_app @@ -22,18 +16,7 @@ def test_every_gateway_module_imports(): importlib.import_module(module.name) -def test_asgi_factory_builds_and_logfire_imports(monkeypatch): - # The exact #602 crash path: configure_logfire only touches the - # logfire package lazily; import it explicitly so a broken logfire - # resolution fails here, not at container startup. - import logfire # noqa: F401 - - from policyengine_simulation_observability.logfire_legacy import ( - configure_logfire, - ) - - monkeypatch.delenv("LOGFIRE_TOKEN", raising=False) - assert configure_logfire("import-coverage-test") is False - +def test_asgi_factory_builds_with_observability_v2(): + assert callable(policyengine_observability.configure) app = create_gateway_app() assert app.routes diff --git a/projects/policyengine-simulation-gateway/tests/test_modal_gateway_image.py b/projects/policyengine-simulation-gateway/tests/test_modal_gateway_image.py index c281ef22d..907630ee0 100644 --- a/projects/policyengine-simulation-gateway/tests/test_modal_gateway_image.py +++ b/projects/policyengine-simulation-gateway/tests/test_modal_gateway_image.py @@ -32,15 +32,17 @@ def pip_install(self, *packages, **kwargs): return self def pip_install_from_requirements(self, requirements_txt, **kwargs): - self.calls.append( - ("pip_install_from_requirements", requirements_txt, kwargs) - ) + self.calls.append(("pip_install_from_requirements", requirements_txt, kwargs)) return self def add_local_python_source(self, *args, **kwargs): self.calls.append(("add_local_python_source", args, kwargs)) return self + def env(self, values): + self.calls.append(("env", values)) + return self + class FakeSecret: @staticmethod @@ -80,9 +82,7 @@ def import_gateway_app(monkeypatch): def test_gateway_image_installs_from_own_lock_via_uv_sync(monkeypatch): app = import_gateway_app(monkeypatch) - uv_sync_calls = [ - call for call in app.gateway_image.calls if call[0] == "uv_sync" - ] + uv_sync_calls = [call for call in app.gateway_image.calls if call[0] == "uv_sync"] assert len(uv_sync_calls) == 1 _, uv_project_dir, kwargs = uv_sync_calls[0] # Absolute project dir: Modal resolves relative dirs against the @@ -106,9 +106,7 @@ def test_gateway_image_mounts_local_packages(monkeypatch): app = import_gateway_app(monkeypatch) mounts = [ - call - for call in app.gateway_image.calls - if call[0] == "add_local_python_source" + call for call in app.gateway_image.calls if call[0] == "add_local_python_source" ] # Dropping a package from this tuple crashes the gateway at import # time — the dev-group path deps are NOT installed in the image. @@ -126,10 +124,28 @@ def test_web_app_secrets(monkeypatch): function_kwargs = {name: kwargs for name, kwargs in app.app.function_calls} assert function_kwargs["web_app"]["secrets"] == [ app.gateway_auth_secret, - app.logfire_secret, ] +def test_gateway_image_has_api_v1_otel_configuration(monkeypatch): + monkeypatch.setenv("OBSERVABILITY_SERVICE_NAMESPACE", "policyengine.api-v1") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://collector.test") + monkeypatch.setenv( + "OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER", + "projects/123/locations/global/workloadIdentityPools/test/providers/test", + ) + app = import_gateway_app(monkeypatch) + env_calls = [call for call in app.gateway_image.calls if call[0] == "env"] + + assert len(env_calls) == 1 + environment = env_calls[0][1] + assert environment["OTEL_EXPORTER_OTLP_ENDPOINT"] == "https://collector.test" + assert environment["OBSERVABILITY_SERVICE_NAMESPACE"] == "policyengine.api-v1" + assert environment["OTEL_TRACES_EXPORTER"] == "otlp" + assert environment["OTEL_METRICS_EXPORTER"] == "otlp" + assert "OBSERVABILITY_GOOGLE_WORKLOAD_IDENTITY_PROVIDER" in environment + + def test_app_module_imports_at_container_entrypoint_path(monkeypatch): """Modal loads the deployed function's module as /root/app.py. @@ -143,9 +159,7 @@ def test_app_module_imports_at_container_entrypoint_path(monkeypatch): sys.modules["modal"].is_local = lambda: False sys.modules.pop("policyengine_simulation_gateway.app", None) - source_path = ( - PROJECT_ROOT / "src" / "policyengine_simulation_gateway" / "app.py" - ) + source_path = PROJECT_ROOT / "src" / "policyengine_simulation_gateway" / "app.py" code = compile(source_path.read_text(), "/root/app.py", "exec") spec = importlib.util.spec_from_loader( "container_entrypoint_gateway_app", loader=None, origin="/root/app.py" diff --git a/projects/policyengine-simulation-gateway/tests/test_observability.py b/projects/policyengine-simulation-gateway/tests/test_observability.py index 4f6cd0efb..68b4930a1 100644 --- a/projects/policyengine-simulation-gateway/tests/test_observability.py +++ b/projects/policyengine-simulation-gateway/tests/test_observability.py @@ -1,41 +1,38 @@ -"""Gateway observability identity and request-correlation tests.""" - -import json +"""Gateway observability identity and request correlation tests.""" +from fastapi.testclient import TestClient from policyengine_observability import REQUEST_ID_HEADER -from policyengine_observability.runtime import REQUEST_LOGGER from policyengine_simulation_gateway.testing import create_gateway_app -def test_gateway_reads_policyengine_request_id_into_observability_context( - monkeypatch, -): - records = [] - monkeypatch.setattr( - REQUEST_LOGGER, - "info", - lambda message: records.append(json.loads(message)), - ) +def test_gateway_preserves_policyengine_request_id(): app = create_gateway_app() - - from fastapi.testclient import TestClient - response = TestClient(app).get( "/health", headers={REQUEST_ID_HEADER: "request-through-entrypoint"}, ) assert response.status_code == 200 - assert len(records) == 1 - assert records[0]["request_id"] == "request-through-entrypoint" - assert records[0]["service_name"] == "policyengine-simulation-gateway" - assert records[0]["service_role"] == "modal_gateway" + assert response.headers[REQUEST_ID_HEADER] == "request-through-entrypoint" -def test_gateway_uses_its_own_service_name(): +def test_gateway_uses_explicit_api_v1_identity(): app = create_gateway_app() + config = app.state.policyengine_observability.config - assert ( - app.state.policyengine_observability.config.service_name - == "policyengine-simulation-gateway" - ) + assert config.service.name == "policyengine-simulation-gateway" + assert config.service.namespace == "policyengine.api-v1" + assert config.service.role == "modal_gateway" + assert config.deployment.environment == "test" + assert config.deployment.platform == "local" + + +def test_gateway_response_survives_unavailable_collector(monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://invalid.invalid") + monkeypatch.setenv("POLICYENGINE_OTEL_GOOGLE_AUDIENCE", "not-a-url") + app = create_gateway_app() + + response = TestClient(app).get("/health") + + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} diff --git a/projects/policyengine-simulation-gateway/tests/test_spm_routes.py b/projects/policyengine-simulation-gateway/tests/test_spm_routes.py index ad7122b27..6ceaffe94 100644 --- a/projects/policyengine-simulation-gateway/tests/test_spm_routes.py +++ b/projects/policyengine-simulation-gateway/tests/test_spm_routes.py @@ -348,7 +348,7 @@ def test_completed_budget_window_rows_keep_resolved_nulls(mock_modal, client): error=None, created_at="2026-01-01T00:00:00+00:00", updated_at="2026-01-01T00:00:01+00:00", - run_id="batch-run-123", + observability_id="batch-run-123", ) ) diff --git a/projects/policyengine-simulation-gateway/uv.lock b/projects/policyengine-simulation-gateway/uv.lock index 3421fd4ec..4bcfa3ebe 100644 --- a/projects/policyengine-simulation-gateway/uv.lock +++ b/projects/policyengine-simulation-gateway/uv.lock @@ -288,18 +288,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "detect-installer" version = "0.1.0" @@ -331,15 +319,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - [[package]] name = "fastapi" version = "0.115.14" @@ -504,6 +483,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, ] +[[package]] +name = "google-cloud-appengine-logging" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, +] + +[[package]] +name = "google-cloud-logging" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, +] + [[package]] name = "google-cloud-monitoring" version = "2.31.0" @@ -548,6 +590,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "greenlet" version = "3.5.3" @@ -564,39 +611,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, ] +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, +] + [[package]] name = "grpcio" -version = "1.81.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, ] [[package]] name = "grpcio-status" -version = "1.71.2" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677, upload-time = "2025-06-28T04:24:05.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -732,18 +793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -765,24 +814,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "logfire" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/fa/14cbd976e8ff77290c0c87f93c45029785537ffb84e2f5215416786cda11/logfire-4.6.0.tar.gz", hash = "sha256:974bc8f4efd3aa9df2d0c7b1d53b35dc4612ff5cee2be6c40dd65e5c26eab694", size = 533174, upload-time = "2025-09-10T14:56:42.168Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/9c/6384dc69601bfba19c1f5909a472b00cc4b5863f411ecf85be308beacf8f/logfire-4.6.0-py3-none-any.whl", hash = "sha256:d322844db7a2f1935976f2852a07e3753489ecc4e83fa288173b05a453bb1cbf", size = 219556, upload-time = "2025-09-10T14:56:37.278Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -943,15 +974,14 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703, upload-time = "2025-02-04T18:17:13.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955, upload-time = "2025-02-04T18:16:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] @@ -986,55 +1016,37 @@ wheels = [ [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d7/44098bf1ef89fc5810cdbda05faa2ae9322a0dbda4921cdc965dc68a9856/opentelemetry_exporter_otlp_proto_common-1.30.0.tar.gz", hash = "sha256:ddbfbf797e518411857d0ca062c957080279320d6235a279f7b64ced73c13897", size = 19640, upload-time = "2025-02-04T18:17:16.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/54/f4b3de49f8d7d3a78fd6e6e1a6fd27dd342eb4d82c088b9078c6a32c3808/opentelemetry_exporter_otlp_proto_common-1.30.0-py3-none-any.whl", hash = "sha256:5468007c81aa9c44dc961ab2cf368a29d3475977df83b4e30aeed42aa7bc3b38", size = 18747, upload-time = "2025-02-04T18:16:51.512Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/3e/c7246df92c25e6ce95c349ad21597b4471b01ec9471e95d5261f1629fe92/opentelemetry_exporter_otlp_proto_grpc-1.30.0.tar.gz", hash = "sha256:d0f10f0b9b9a383b7d04a144d01cb280e70362cccc613987e234183fd1f01177", size = 26256, upload-time = "2025-02-04T18:17:16.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/35/d9f63fd84c2ed8dbd407bcbb933db4ed6e1b08e7fbdaca080b9ac309b927/opentelemetry_exporter_otlp_proto_grpc-1.30.0-py3-none-any.whl", hash = "sha256:2906bcae3d80acc54fd1ffcb9e44d324e8631058b502ebe4643ca71d1ff30830", size = 18550, upload-time = "2025-02-04T18:16:52.532Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/f9/abb9191d536e6a2e2b7903f8053bf859a76bf784e3ca19a5749550ef19e4/opentelemetry_exporter_otlp_proto_http-1.30.0.tar.gz", hash = "sha256:c3ae75d4181b1e34a60662a6814d0b94dd33b628bee5588a878bed92cee6abdc", size = 15073, upload-time = "2025-02-04T18:17:18.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/3c/cdf34bc459613f2275aff9b258f35acdc4c4938dad161d17437de5d4c034/opentelemetry_exporter_otlp_proto_http-1.30.0-py3-none-any.whl", hash = "sha256:9578e790e579931c5ffd50f1e6975cbdefb6a0a0a5dea127a6ae87df10e0a589", size = 17245, upload-time = "2025-02-04T18:16:53.514Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1042,14 +1054,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5a/4c7f02235ac1269b48f3855f6be1afc641f31d4888d28b90b732fbce7141/opentelemetry_instrumentation-0.51b0.tar.gz", hash = "sha256:4ca266875e02f3988536982467f7ef8c32a38b8895490ddce9ad9604649424fa", size = 27760, upload-time = "2025-02-04T18:21:09.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/2c/48fa93f1acca9f79a06da0df7bfe916632ecc7fce1971067b3e46bcae55b/opentelemetry_instrumentation-0.51b0-py3-none-any.whl", hash = "sha256:c6de8bd26b75ec8b0e54dff59e198946e29de6a10ec65488c357d4b34aa5bdcf", size = 30923, upload-time = "2025-02-04T18:19:37.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -1058,14 +1070,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/67/8aa6e1129f641f0f3f8786e6c5d18c1f2bbe490bd4b0e91a6879e85154d2/opentelemetry_instrumentation_asgi-0.51b0.tar.gz", hash = "sha256:b3fe97c00f0bfa934371a69674981d76591c68d937b6422a5716ca21081b4148", size = 24201, upload-time = "2025-02-04T18:21:14.321Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/0a95ab37302729543631a789ba8e71dea75c520495739dbbbdfdc580b401/opentelemetry_instrumentation_asgi-0.51b0-py3-none-any.whl", hash = "sha256:e8072993db47303b633c6ec1bc74726ba4d32bd0c46c28dfadf99f79521a324c", size = 16340, upload-time = "2025-02-04T18:19:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1074,43 +1086,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/8db4422b5084177d1ef6c7855c69bf2e9e689f595a4a9b59e60588e0d427/opentelemetry_instrumentation_fastapi-0.51b0.tar.gz", hash = "sha256:1624e70f2f4d12ceb792d8a0c331244cd6723190ccee01336273b4559bc13abc", size = 19249, upload-time = "2025-02-04T18:21:28.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117, upload-time = "2025-02-04T18:20:15.267Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-httpx" -version = "0.51b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/d5/4a3990c461ae7e55212115e0f8f3aa412b5ce6493579e85c292245ac69ea/opentelemetry_instrumentation_httpx-0.51b0.tar.gz", hash = "sha256:061d426a04bf5215a859fea46662e5074f920e5cbde7e6ad6825a0a1b595802c", size = 17700, upload-time = "2025-02-04T18:21:31.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ba/23d4ab6402408c01f1c3f32e0c04ea6dae575bf19bcb9a0049c9e768c983/opentelemetry_instrumentation_httpx-0.51b0-py3-none-any.whl", hash = "sha256:2e3fdf755ba6ead6ab43031497c3d55d4c796d0368eccc0ce48d304b7ec6486a", size = 14109, upload-time = "2025-02-04T18:20:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, ] [[package]] name = "opentelemetry-instrumentation-logging" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/01/922f37e5d8c74bc3f92f24f3e240879ae1d04710fdeebb74f39914fc8467/opentelemetry_instrumentation_logging-0.51b0.tar.gz", hash = "sha256:2adaba90fab85a80de24728153b42ef2a74dd80fda5a3b17f3f95c11bb27c3ff", size = 9754, upload-time = "2025-02-04T18:21:33.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/e3/2d2390370b8730b6b9a2341892cd5508d0f5fa3fe30977457f89979d4085/opentelemetry_instrumentation_logging-0.51b0-py3-none-any.whl", hash = "sha256:b0855cf8b3e37ae4fdb5b77982fbfa713778762c58db3071eb3ca6c97456a049", size = 12171, upload-time = "2025-02-04T18:20:25.006Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1119,21 +1116,21 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/b2/970b1b46576b663bba64503486afe266c064c2bfd1862876420714ce29d9/opentelemetry_instrumentation_sqlalchemy-0.51b0.tar.gz", hash = "sha256:dbfe95b69006017f903dda194606be458d54789e6b3419d37161fb8861bb98a5", size = 14582, upload-time = "2025-02-04T18:21:46.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9d/72af21527ce6f286ac78dc2c75ce44b76cc45343a28f0bcbb13f213421fc/opentelemetry_instrumentation_sqlalchemy-0.65b0.tar.gz", hash = "sha256:8ec2e79f1e00808c5dc639ab5b2cfcdf9dcdef55efd6442c6019707fe2894028", size = 18007, upload-time = "2026-07-16T15:26:19.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/d4/b68c3b3388dd5107f3ed532747e112249c152ba44af71a1f96673d66e3ee/opentelemetry_instrumentation_sqlalchemy-0.51b0-py3-none-any.whl", hash = "sha256:5ff4816228b496aef1511149e2b17a25e0faacec4d5eb65bf18a9964af40f1af", size = 14132, upload-time = "2025-02-04T18:20:50.513Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/3eb3195a60b894cc1852a5657a2b64764a998085c50d7fb3452148af8f18/opentelemetry_instrumentation_sqlalchemy-0.65b0-py3-none-any.whl", hash = "sha256:4d8a2e5afc7b505a48d05cb1fb6db5f8b31681814c1d7767bd6d4b5bdd3a3047", size = 14410, upload-time = "2026-07-16T15:25:32.04Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/6e/c1ff2e3b0cd3a189a6be03fd4d63441d73d7addd9117ab5454e667b9b6c7/opentelemetry_proto-1.30.0.tar.gz", hash = "sha256:afe5c9c15e8b68d7c469596e5b32e8fc085eb9febdd6fb4e20924a93a0389179", size = 34362, upload-time = "2025-02-04T18:17:28.099Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/d7/85de6501f7216995295f7ec11e470142e6a6e080baacec1753bbf272e007/opentelemetry_proto-1.30.0-py3-none-any.whl", hash = "sha256:c6290958ff3ddacc826ca5abbeb377a31c2334387352a259ba0df37c243adc11", size = 55854, upload-time = "2025-02-04T18:17:08.024Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] @@ -1153,38 +1150,38 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.30.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633, upload-time = "2025-02-04T18:17:28.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717, upload-time = "2025-02-04T18:17:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191, upload-time = "2025-02-04T18:17:29.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416, upload-time = "2025-02-04T18:17:11.305Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.51b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/64/32510c0a803465eb6ef1f5bd514d0f5627f8abc9444ed94f7240faf6fcaa/opentelemetry_util_http-0.51b0.tar.gz", hash = "sha256:05edd19ca1cc3be3968b1e502fd94816901a365adbeaab6b6ddb974384d3a0b9", size = 8043, upload-time = "2025-02-04T18:21:59.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/dd/c371eeb9cc78abbdad231a27ce1a196a37ef96328d876ccbb381dea4c8ee/opentelemetry_util_http-0.51b0-py3-none-any.whl", hash = "sha256:0561d7a6e9c422b9ef9ae6e77eafcfcd32a2ab689f5e801475cbb67f189efa20", size = 7304, upload-time = "2025-02-04T18:21:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] [[package]] @@ -1247,10 +1244,10 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.8,<0.116.0" }, { name = "opentelemetry-exporter-gcp-monitoring", specifier = ">=1.9.0a0,<2.0.0" }, { name = "opentelemetry-exporter-gcp-trace", specifier = ">=1.9.0,<2.0.0" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-logging", specifier = ">=0.51b0,<0.52" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.51b0" }, - { name = "opentelemetry-sdk", specifier = ">=1.30.0,<2.0.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0,<0.66" }, + { name = "opentelemetry-sdk", specifier = ">=1.44.0,<2.0.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1264,25 +1261,26 @@ provides-extras = ["test", "build"] [[package]] name = "policyengine-observability" -version = "1.3.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-httpx" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2a/5e0e4c0f30b20a59ffff14364e15374f9eee494f658f41559aba97c152a2/policyengine_observability-1.3.0.tar.gz", hash = "sha256:4c2f3bb2ee59886aef6c90f1edb9c0b957ace75b1518616c0e61905b8be431e0", size = 107961, upload-time = "2026-07-01T21:10:53.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/69/963d1044a8519e2967b473db1ff1fe304966c9f93396660252bd3e339be5/policyengine_observability-3.0.0.tar.gz", hash = "sha256:bcf8027e2526c22ac490e0ef5f9e1fe89254e08b479611fa16a94142e54c34d9", size = 122222, upload-time = "2026-09-23T10:58:47.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/a2/80e4bc1c4923e8f4c04483bfd22df2500b0d132c4ce1a6dec6749c94d947/policyengine_observability-1.3.0-py3-none-any.whl", hash = "sha256:7a9444e2cb8ddd7f6d54d601bd1e0a7651bd44783b78627b293a27e741f91930", size = 31087, upload-time = "2026-07-01T21:10:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/8174cbb0378573553fc6eea0073063fb4bcc197cbdbbc2170f0e73bba57a/policyengine_observability-3.0.0-py3-none-any.whl", hash = "sha256:8ce4cf186c81ba136fd824c6c81c78d4b637da177c6ac657d15ffb538fe88d75", size = 40200, upload-time = "2026-09-23T10:58:45.799Z" }, ] [package.optional-dependencies] fastapi = [ { name = "fastapi" }, ] +google = [ + { name = "google-auth" }, + { name = "google-cloud-logging" }, +] +otlp-grpc = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] [[package]] name = "policyengine-simulation-contract" @@ -1306,15 +1304,13 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, - { name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, - { name = "psycopg", extras = ["binary"], marker = "extra == 'test'", specifier = ">=3.2,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.1.1" }, ] -provides-extras = ["modal", "postgres", "test", "build"] +provides-extras = ["modal", "test", "build"] [[package]] name = "policyengine-simulation-gateway" @@ -1323,10 +1319,9 @@ source = { editable = "." } dependencies = [ { name = "cryptography" }, { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, { name = "modal" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "packaging" }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, { name = "pyjwt" }, ] @@ -1357,11 +1352,10 @@ requires-dist = [ { name = "cryptography", specifier = ">=41.0.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "packaging", specifier = ">=24" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, @@ -1384,9 +1378,7 @@ version = "0.1.0" source = { editable = "../../libs/policyengine-simulation-observability" } dependencies = [ { name = "fastapi" }, - { name = "importlib-metadata" }, - { name = "logfire" }, - { name = "policyengine-observability", extra = ["fastapi"] }, + { name = "policyengine-observability", extra = ["fastapi", "google", "otlp-grpc"] }, { name = "pydantic" }, ] @@ -1395,9 +1387,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "importlib-metadata", specifier = ">=8" }, - { name = "logfire", specifier = ">=3.0.0" }, - { name = "policyengine-observability", extras = ["fastapi"], specifier = ">=1.3.0,<2" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, @@ -1463,16 +1453,17 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.6" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, - { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, - { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -2083,12 +2074,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] - -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -]