diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d39522b3e..c15ad8b320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -545,6 +545,9 @@ jobs: #- name: Lint # run: npx @biomejs/biome check sdk/packages/node + - name: Build observability (prerequisite for type check) + run: pnpm --filter @iii-dev/observability build + - name: Type check run: pnpm --filter iii-sdk exec tsc --noEmit diff --git a/.github/workflows/release-iii.yml b/.github/workflows/release-iii.yml index 15af66830e..db4b6526e9 100644 --- a/.github/workflows/release-iii.yml +++ b/.github/workflows/release-iii.yml @@ -393,7 +393,7 @@ jobs: sdk-npm: name: SDK Node Publish - needs: [setup, create-iii-release] + needs: [setup, create-iii-release, observability-npm] if: ${{ !failure() && !cancelled() }} uses: ./.github/workflows/_npm.yml with: @@ -410,7 +410,7 @@ jobs: sdk-npm-browser: name: SDK Browser Publish - needs: [setup, create-iii-release] + needs: [setup, create-iii-release, observability-npm] if: ${{ !failure() && !cancelled() }} uses: ./.github/workflows/_npm.yml with: @@ -425,9 +425,26 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + observability-npm: + name: Observability Node Publish + needs: [setup, create-iii-release] + if: ${{ !failure() && !cancelled() }} + uses: ./.github/workflows/_npm.yml + with: + package_path: sdk/packages/node/observability + npm_tag: ${{ needs.setup.outputs.npm_tag }} + build_filter: "@iii-dev/observability" + dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} + slack_thread_ts: ${{ needs.setup.outputs.slack_ts }} + slack_label: Observability Node (npm) + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + sdk-py: name: SDK Python Publish - needs: [setup, create-iii-release] + needs: [setup, create-iii-release, observability-py] if: ${{ !failure() && !cancelled() }} uses: ./.github/workflows/_py.yml with: @@ -440,9 +457,24 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + observability-py: + name: Observability Python Publish + needs: [setup, create-iii-release] + if: ${{ !failure() && !cancelled() }} + uses: ./.github/workflows/_py.yml + with: + package_path: sdk/packages/python/observability + dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} + slack_thread_ts: ${{ needs.setup.outputs.slack_ts }} + slack_label: Observability Python (pypi) + secrets: + PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + sdk-rust: name: SDK Rust Publish - needs: [setup, create-iii-release] + needs: [setup, create-iii-release, observability-rust] if: ${{ !failure() && !cancelled() }} uses: ./.github/workflows/_rust-cargo.yml with: @@ -455,6 +487,21 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + observability-rust: + name: Observability Rust Publish + needs: [setup, create-iii-release] + if: ${{ !failure() && !cancelled() }} + uses: ./.github/workflows/_rust-cargo.yml + with: + package_path: sdk/packages/rust/observability + dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} + slack_thread_ts: ${{ needs.setup.outputs.slack_ts }} + slack_label: Observability Rust (cargo) + secrets: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + # ────────────────────────────────────────────────────────────── # Docker # ────────────────────────────────────────────────────────────── @@ -589,6 +636,9 @@ jobs: - sdk-npm-browser - sdk-py - sdk-rust + - observability-npm + - observability-py + - observability-rust - docker-engine - homebrew-engine - homebrew-console @@ -612,6 +662,9 @@ jobs: SDK_NPM_BROWSER_RESULT: ${{ needs.sdk-npm-browser.result }} SDK_PY_RESULT: ${{ needs.sdk-py.result }} SDK_RUST_RESULT: ${{ needs.sdk-rust.result }} + OBS_NPM_RESULT: ${{ needs.observability-npm.result }} + OBS_PY_RESULT: ${{ needs.observability-py.result }} + OBS_RUST_RESULT: ${{ needs.observability-rust.result }} DOCKER_RESULT: ${{ needs.docker-engine.result }} BREW_ENGINE_RESULT: ${{ needs.homebrew-engine.result }} BREW_CONSOLE_RESULT: ${{ needs.homebrew-console.result }} @@ -638,6 +691,9 @@ jobs: LINES="$LINES$(icon "$SDK_NPM_BROWSER_RESULT") SDK Browser\n" LINES="$LINES$(icon "$SDK_PY_RESULT") SDK Python\n" LINES="$LINES$(icon "$SDK_RUST_RESULT") SDK Rust\n" + LINES="$LINES$(icon "$OBS_NPM_RESULT") Observability Node\n" + LINES="$LINES$(icon "$OBS_PY_RESULT") Observability Python\n" + LINES="$LINES$(icon "$OBS_RUST_RESULT") Observability Rust\n" LINES="$LINES$(icon "$BUILTIN_WORKERS_RESULT") Builtin Workers Registry\n" LINES="$LINES$(icon "$WORKER_SKILLS_RESULT") Worker Skills Registry\n" @@ -647,7 +703,7 @@ jobs: LINES="$LINES$(icon "$BREW_CONSOLE_RESULT") Homebrew (iii-console)\n" fi - for r in "$INIT_RESULT" "$FIRMWARE_RESULT" "$ENGINE_RESULT" "$WORKER_RESULT" "$CONSOLE_RESULT" "$SDK_NPM_RESULT" "$SDK_NPM_BROWSER_RESULT" "$SDK_PY_RESULT" "$SDK_RUST_RESULT" "$BUILTIN_WORKERS_RESULT" "$WORKER_SKILLS_RESULT"; do + for r in "$INIT_RESULT" "$FIRMWARE_RESULT" "$ENGINE_RESULT" "$WORKER_RESULT" "$CONSOLE_RESULT" "$SDK_NPM_RESULT" "$SDK_NPM_BROWSER_RESULT" "$SDK_PY_RESULT" "$SDK_RUST_RESULT" "$OBS_NPM_RESULT" "$OBS_PY_RESULT" "$OBS_RUST_RESULT" "$BUILTIN_WORKERS_RESULT" "$WORKER_SKILLS_RESULT"; do [[ "$r" != "success" && "$r" != "skipped" ]] && OVERALL="failure" done diff --git a/Cargo.lock b/Cargo.lock index 2378a1580b..ea69235c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2636,6 +2636,7 @@ dependencies = [ "hostname", "http-body-util", "iana-time-zone", + "iii-observability", "iii-sdk", "iii-worker", "indexmap 2.14.0", @@ -2697,6 +2698,7 @@ dependencies = [ "axum", "clap", "futures-util", + "iii-observability", "iii-sdk", "mime_guess", "reqwest 0.12.28", @@ -2715,6 +2717,7 @@ name = "iii-example" version = "0.0.0" dependencies = [ "async-trait", + "iii-observability", "iii-sdk", "reqwest 0.12.28", "schemars 0.8.22", @@ -2767,20 +2770,18 @@ dependencies = [ ] [[package]] -name = "iii-sdk" -version = "0.14.0-next.2" +name = "iii-observability" +version = "0.13.0-next.1" dependencies = [ "async-trait", "ctor", "futures-util", - "hostname", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", "prost", "reqwest 0.12.28", - "schemars 0.8.22", "serde", "serde_json", "serial_test", @@ -2789,6 +2790,29 @@ dependencies = [ "tokio", "tokio-tungstenite 0.28.0", "tracing", + "uuid", +] + +[[package]] +name = "iii-sdk" +version = "0.14.0-next.2" +dependencies = [ + "async-trait", + "ctor", + "futures-util", + "hostname", + "iii-observability", + "opentelemetry", + "opentelemetry_sdk", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "serial_test", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite 0.28.0", + "tracing", "tracing-test", "uuid", ] @@ -2853,6 +2877,7 @@ dependencies = [ "humantime", "iii-filesystem", "iii-network", + "iii-observability", "iii-sdk", "iii-shell-client", "iii-shell-proto", diff --git a/Cargo.toml b/Cargo.toml index 56d4ab2515..ea83ff3240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "engine/function-macros", "sdk/packages/rust/iii", "sdk/packages/rust/iii-example", + "sdk/packages/rust/observability", "console/packages/console-rust", "crates/iii-init", "crates/iii-filesystem", @@ -26,6 +27,7 @@ homepage = "https://iii.dev" [workspace.dependencies] iii-sdk = { path = "sdk/packages/rust/iii" } +iii-observability = { path = "sdk/packages/rust/observability", version = "0.13.0-next.1" } scaffolder-core = { path = "crates/scaffolder-core" } # Shared dependencies used by tooling crates diff --git a/console/packages/console-rust/Cargo.toml b/console/packages/console-rust/Cargo.toml index 378124d005..2b47a00225 100644 --- a/console/packages/console-rust/Cargo.toml +++ b/console/packages/console-rust/Cargo.toml @@ -18,6 +18,7 @@ tokio-tungstenite = "0.28" futures-util = { version = "0.3", default-features = false, features = ["sink"] } tower-http = { version = "0.6.8", features = ["cors"] } iii-sdk = { workspace = true } +iii-observability = { workspace = true } # Async runtime tokio = { version = "1", features = ["full"] } diff --git a/console/packages/console-rust/src/main.rs b/console/packages/console-rust/src/main.rs index 28606a343d..f9a7a68eb2 100644 --- a/console/packages/console-rust/src/main.rs +++ b/console/packages/console-rust/src/main.rs @@ -97,7 +97,7 @@ async fn main() -> Result<()> { "OpenTelemetry enabled (service: {})", args.otel_service_name ); - Some(iii_sdk::OtelConfig { + Some(iii_observability::OtelConfig { enabled: Some(true), service_name: Some(args.otel_service_name), service_version: Some(env!("CARGO_PKG_VERSION").to_string()), diff --git a/crates/iii-worker/Cargo.toml b/crates/iii-worker/Cargo.toml index 3f01562e53..6007f5f451 100644 --- a/crates/iii-worker/Cargo.toml +++ b/crates/iii-worker/Cargo.toml @@ -26,6 +26,7 @@ iii-filesystem = { path = "../iii-filesystem" } iii-network = { path = "../iii-network" } # TODO: switch to crates.io pin before shipping; path dep for co-development iii-sdk = { path = "../../sdk/packages/rust/iii" } +iii-observability = { workspace = true } iii-shell-client = { path = "../iii-shell-client" } iii-shell-proto = { path = "../iii-shell-proto" } iii-supervisor = { path = "../iii-supervisor" } diff --git a/crates/iii-worker/src/cli/worker_manager_daemon.rs b/crates/iii-worker/src/cli/worker_manager_daemon.rs index 860b0b0a63..1dddd7b89a 100644 --- a/crates/iii-worker/src/cli/worker_manager_daemon.rs +++ b/crates/iii-worker/src/cli/worker_manager_daemon.rs @@ -23,9 +23,9 @@ use crate::core::{ list as core_list, remove as core_remove, start as core_start, stop as core_stop, update as core_update, }; +use iii_observability::OtelConfig; use iii_sdk::{ - III, InitOptions, OtelConfig, RegisterFunction, RegisterTriggerType, WorkerMetadata, - register_worker, + III, InitOptions, RegisterFunction, RegisterTriggerType, WorkerMetadata, register_worker, }; use schemars::{JsonSchema, schema_for}; use serde_json::Value; diff --git a/crates/iii-worker/src/sandbox_daemon/mod.rs b/crates/iii-worker/src/sandbox_daemon/mod.rs index 45639ec5bf..7c83e164b8 100644 --- a/crates/iii-worker/src/sandbox_daemon/mod.rs +++ b/crates/iii-worker/src/sandbox_daemon/mod.rs @@ -21,7 +21,8 @@ pub use registry::SandboxRegistry; use std::sync::Arc; -use iii_sdk::{InitOptions, OtelConfig, RegisterFunction, WorkerMetadata, register_worker}; +use iii_observability::OtelConfig; +use iii_sdk::{InitOptions, RegisterFunction, WorkerMetadata, register_worker}; use crate::sandbox_daemon::config::SandboxConfig; use crate::sandbox_daemon::errors::SandboxErrorWire; diff --git a/engine/Cargo.toml b/engine/Cargo.toml index abaf3778d2..46a99f76ff 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -91,6 +91,7 @@ rabbitmq = ["lapin"] [dependencies] function-macros = { path = "function-macros" } iii-sdk = { workspace = true } +iii-observability = { workspace = true } scaffolder-core = { workspace = true } console = { workspace = true } ctrlc = { workspace = true } diff --git a/engine/src/workers/observability/otel.rs b/engine/src/workers/observability/otel.rs index 99000b8bb2..0f8af3c92b 100644 --- a/engine/src/workers/observability/otel.rs +++ b/engine/src/workers/observability/otel.rs @@ -739,7 +739,7 @@ where let _ = IN_MEMORY_STORAGE.set(memory_storage); SdkTracerProvider::builder() - .with_span_processor(iii_sdk::BaggageSpanProcessor::default()) + .with_span_processor(iii_observability::BaggageSpanProcessor::default()) .with_batch_exporter(exporter) .with_sampler(sampler) .with_id_generator(RandomIdGenerator::default()) @@ -758,7 +758,7 @@ where config.service_name.clone(), ); SdkTracerProvider::builder() - .with_span_processor(iii_sdk::BaggageSpanProcessor::default()) + .with_span_processor(iii_observability::BaggageSpanProcessor::default()) .with_simple_exporter(exporter) .with_sampler(sampler) .with_id_generator(RandomIdGenerator::default()) @@ -772,7 +772,7 @@ where InMemorySpanExporter::new(config.memory_max_spans, config.service_name.clone()); SdkTracerProvider::builder() - .with_span_processor(iii_sdk::BaggageSpanProcessor::default()) + .with_span_processor(iii_observability::BaggageSpanProcessor::default()) .with_simple_exporter(exporter) .with_sampler(sampler) .with_id_generator(RandomIdGenerator::default()) @@ -803,6 +803,7 @@ where ); SdkTracerProvider::builder() + .with_span_processor(iii_observability::BaggageSpanProcessor::default()) .with_batch_exporter(tee_exporter) .with_sampler(sampler) .with_id_generator(RandomIdGenerator::default()) @@ -821,7 +822,7 @@ where config.service_name.clone(), ); SdkTracerProvider::builder() - .with_span_processor(iii_sdk::BaggageSpanProcessor::default()) + .with_span_processor(iii_observability::BaggageSpanProcessor::default()) .with_simple_exporter(exporter) .with_sampler(sampler) .with_id_generator(RandomIdGenerator::default()) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5ef6e6840..e909ad087a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,43 +163,25 @@ importers: sdk/packages/node/iii: dependencies: + '@iii-dev/observability': + specifier: workspace:* + version: link:../observability '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 - '@opentelemetry/api-logs': - specifier: ^0.57.0 - version: 0.57.2 - '@opentelemetry/core': - specifier: ^1.30.0 - version: 1.30.1(@opentelemetry/api@1.9.0) + ws: + specifier: ^8.18.3 + version: 8.19.0 + devDependencies: + '@opentelemetry/context-async-hooks': + specifier: ^2.7.1 + version: 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: ^0.57.0 version: 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': - specifier: ^0.57.0 - version: 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': - specifier: ^1.30.0 - version: 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': - specifier: ^0.57.0 - version: 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': - specifier: ^1.30.0 - version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: ^1.30.0 version: 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': - specifier: ^1.30.0 - version: 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': - specifier: ^1.28.0 - version: 1.40.0 - ws: - specifier: ^8.18.3 - version: 8.19.0 - devDependencies: '@types/ws': specifier: ^8.18.1 version: 8.18.1 @@ -259,6 +241,61 @@ importers: specifier: ^5.9.3 version: 5.9.3 + sdk/packages/node/observability: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/api-logs': + specifier: ^0.57.0 + version: 0.57.2 + '@opentelemetry/core': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.30.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.40.0 + ws: + specifier: ^8.18.3 + version: 8.19.0 + devDependencies: + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + '@vitest/coverage-v8': + specifier: ^2.1.0 + version: 2.1.9(vitest@2.1.9(@edge-runtime/vm@3.2.0)(@types/node@25.3.3)(happy-dom@17.6.3)(lightningcss@1.31.1)) + tsdown: + specifier: ^0.21.4 + version: 0.21.4(synckit@0.11.12)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@edge-runtime/vm@3.2.0)(@types/node@25.3.3)(happy-dom@17.6.3)(lightningcss@1.31.1) + website: devDependencies: browser-sync: @@ -1049,6 +1086,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.30.1': resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} engines: {node: '>=14'} @@ -5517,6 +5560,10 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -6459,7 +6506,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.11.0 + '@types/node': 25.3.3 '@ungap/structured-clone@1.3.1': {} @@ -8549,7 +8596,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.11.0 + '@types/node': 25.3.3 long: 5.3.2 punycode.js@2.3.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1a92121b43..b73239c978 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - sdk/packages/node/iii - sdk/packages/node/iii-example - sdk/packages/node/iii-browser + - sdk/packages/node/observability - console/packages/* - docs - website diff --git a/sdk/packages/node/iii/package.json b/sdk/packages/node/iii/package.json index 8d5da7e5d6..7bf56fb405 100644 --- a/sdk/packages/node/iii/package.json +++ b/sdk/packages/node/iii/package.json @@ -36,28 +36,17 @@ "types": "./dist/state.d.ts", "import": "./dist/state.mjs", "require": "./dist/state.cjs" - }, - "./telemetry": { - "types": "./dist/telemetry.d.ts", - "import": "./dist/telemetry.mjs", - "require": "./dist/telemetry.cjs" } }, "dependencies": { + "@iii-dev/observability": "workspace:*", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.57.0", - "@opentelemetry/core": "^1.30.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/otlp-transformer": "^0.57.0", - "@opentelemetry/resources": "^1.30.0", - "@opentelemetry/sdk-logs": "^0.57.0", - "@opentelemetry/sdk-metrics": "^1.30.0", - "@opentelemetry/sdk-trace-base": "^1.30.0", - "@opentelemetry/sdk-trace-node": "^1.30.0", - "@opentelemetry/semantic-conventions": "^1.28.0", "ws": "^8.18.3" }, "devDependencies": { + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/sdk-trace-base": "^1.30.0", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^2.1.0", "tsdown": "^0.21.4", diff --git a/sdk/packages/node/iii/src/iii.ts b/sdk/packages/node/iii/src/iii.ts index 8cf6ea569e..cca72b720c 100644 --- a/sdk/packages/node/iii/src/iii.ts +++ b/sdk/packages/node/iii/src/iii.ts @@ -26,7 +26,7 @@ import { type TriggerRequest, type WorkerRegisteredMessage, } from './iii-types' -import { registerWorkerGauges, stopWorkerGauges } from './otel-worker-gauges' +import { registerWorkerGauges, stopWorkerGauges } from '@iii-dev/observability' import type { IStream } from './stream' import { detectProjectName } from './utils' import { @@ -45,7 +45,7 @@ import { shutdownOtel, SpanKind, withSpan, -} from './telemetry-system' +} from '@iii-dev/observability' import type { TriggerHandler } from './triggers' import type { FunctionRef, diff --git a/sdk/packages/node/iii/src/index.ts b/sdk/packages/node/iii/src/index.ts index f75a4c71be..5442c62cac 100644 --- a/sdk/packages/node/iii/src/index.ts +++ b/sdk/packages/node/iii/src/index.ts @@ -28,7 +28,7 @@ export type { TriggerRequest, } from './iii-types' -export { Logger } from './logger' +export { Logger } from '@iii-dev/observability' export type { TriggerConfig, TriggerHandler } from './triggers' diff --git a/sdk/packages/node/iii/src/types.ts b/sdk/packages/node/iii/src/types.ts index cf3b73278a..d7a23670d4 100644 --- a/sdk/packages/node/iii/src/types.ts +++ b/sdk/packages/node/iii/src/types.ts @@ -26,34 +26,6 @@ import type { TriggerHandler } from './triggers' // biome-ignore lint/suspicious/noExplicitAny: generic defaults require any for contravariant compatibility export type RemoteFunctionHandler = (data: TInput) => Promise -/** OTEL Log Event from the engine */ -export type OtelLogEvent = { - /** Timestamp in Unix nanoseconds */ - timestamp_unix_nano: number - /** Observed timestamp in Unix nanoseconds */ - observed_timestamp_unix_nano: number - /** OTEL severity number (1-24): TRACE=1-4, DEBUG=5-8, INFO=9-12, WARN=13-16, ERROR=17-20, FATAL=21-24 */ - severity_number: number - /** Severity text (e.g., "INFO", "WARN", "ERROR") */ - severity_text: string - /** Log message body */ - body: string - /** Structured attributes */ - attributes: Record - /** Trace ID for correlation (if available) */ - trace_id?: string - /** Span ID for correlation (if available) */ - span_id?: string - /** Resource attributes from the emitting service */ - resource: Record - /** Service name that emitted the log */ - service_name: string - /** Instrumentation scope name (if available) */ - instrumentation_scope_name?: string - /** Instrumentation scope version (if available) */ - instrumentation_scope_version?: string -} - // biome-ignore lint/suspicious/noExplicitAny: generic default requires any for contravariant compatibility export type Invocation = { resolve: (data: TOutput) => void diff --git a/sdk/packages/node/iii/src/utils.ts b/sdk/packages/node/iii/src/utils.ts index f2be4b3897..fe468c5b6c 100644 --- a/sdk/packages/node/iii/src/utils.ts +++ b/sdk/packages/node/iii/src/utils.ts @@ -30,35 +30,6 @@ export function detectProjectName(cwd: string = process.cwd()): string | undefin return base || undefined } -/** - * Safely stringify a value, handling circular references, BigInt, and other edge cases. - * Returns "[unserializable]" if serialization fails for any reason. - */ -export function safeStringify(value: unknown): string { - const seen = new WeakSet() - - try { - return JSON.stringify(value, (_key, val) => { - // Handle BigInt - if (typeof val === 'bigint') { - return val.toString() - } - - // Handle circular references - if (val !== null && typeof val === 'object') { - if (seen.has(val)) { - return '[Circular]' - } - seen.add(val) - } - - return val - }) - } catch { - return '[unserializable]' - } -} - /** * Helper that wraps an HTTP-style handler (with separate `req`/`res` arguments) * into the function handler format expected by the SDK. diff --git a/sdk/packages/node/iii/tests/baggage-span-processor.test.ts b/sdk/packages/node/iii/tests/baggage-span-processor.test.ts index c8cf4e6009..b640fe4f9d 100644 --- a/sdk/packages/node/iii/tests/baggage-span-processor.test.ts +++ b/sdk/packages/node/iii/tests/baggage-span-processor.test.ts @@ -9,7 +9,7 @@ import { } from '@opentelemetry/sdk-trace-base' import { describe, expect, it } from 'vitest' -import { BaggageSpanProcessor, DEFAULT_ALLOWLIST } from '../src/telemetry-system/baggage-span-processor' +import { BaggageSpanProcessor, DEFAULT_ALLOWLIST } from '@iii-dev/observability' function buildTestProvider(processor: BaggageSpanProcessor) { const exporter = new InMemorySpanExporter() diff --git a/sdk/packages/node/iii/tests/bridge-utils.ts b/sdk/packages/node/iii/tests/bridge-utils.ts index ab3c329246..3b8271f690 100644 --- a/sdk/packages/node/iii/tests/bridge-utils.ts +++ b/sdk/packages/node/iii/tests/bridge-utils.ts @@ -10,6 +10,13 @@ export const bridgeIII = registerWorker(BRIDGE_WS_URL, { initialDelayMs: 100, maxDelayMs: 1000, }, + otel: { + reconnectionConfig: { + maxRetries: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + }, + }, }) export const logger = new Logger() diff --git a/sdk/packages/node/iii/tests/exports.test.ts b/sdk/packages/node/iii/tests/exports.test.ts index 3585d86c63..60ee05c262 100644 --- a/sdk/packages/node/iii/tests/exports.test.ts +++ b/sdk/packages/node/iii/tests/exports.test.ts @@ -1,6 +1,5 @@ import { beforeAll, describe, expect, it, vi } from 'vitest' import { registerWorker, Logger } from '../src/index' -import { initOtel, shutdownOtel, getLogger } from '../src/telemetry' import { iii } from './utils' beforeAll(() => { @@ -24,21 +23,4 @@ describe('Package Exports', () => { expect(stateModule.StateEventType).toBeDefined() expect(Object.keys(stateModule).length).toBeGreaterThan(0) }) - - it('should export supported telemetry utilities', () => { - expect(initOtel).toBeDefined() - expect(typeof initOtel).toBe('function') - expect(shutdownOtel).toBeDefined() - expect(typeof shutdownOtel).toBe('function') - expect(getLogger).toBeDefined() - expect(typeof getLogger).toBe('function') - }) - - it('should not expose internal tracer/meter accessors on public telemetry surface', async () => { - const telemetryExports = await import('../src/telemetry') - const keys = Object.keys(telemetryExports) - expect(keys).not.toContain('getTracer') - expect(keys).not.toContain('getMeter') - expect(keys).not.toContain('SpanStatusCode') - }) }) diff --git a/sdk/packages/node/iii/tests/fetch-instrumentation.test.ts b/sdk/packages/node/iii/tests/fetch-instrumentation.test.ts index 109823490d..61ea2c5c9e 100644 --- a/sdk/packages/node/iii/tests/fetch-instrumentation.test.ts +++ b/sdk/packages/node/iii/tests/fetch-instrumentation.test.ts @@ -24,6 +24,7 @@ vi.mock('ws', () => { vi.mock('@opentelemetry/sdk-trace-node', () => ({ NodeTracerProvider: vi.fn().mockImplementation(() => ({ register: vi.fn(), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), })) @@ -32,6 +33,7 @@ vi.mock('@opentelemetry/sdk-trace-node', () => ({ vi.mock('@opentelemetry/sdk-metrics', () => ({ MeterProvider: vi.fn().mockImplementation(() => ({ getMeter: vi.fn().mockReturnValue({ createCounter: vi.fn() }), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), PeriodicExportingMetricReader: vi.fn().mockImplementation(() => ({})), @@ -42,25 +44,38 @@ vi.mock('@opentelemetry/sdk-logs', () => ({ LoggerProvider: vi.fn().mockImplementation(() => ({ getLogger: vi.fn().mockReturnValue({ emit: vi.fn() }), addLogRecordProcessor: vi.fn(), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), SimpleLogRecordProcessor: vi.fn().mockImplementation(() => ({})), + BatchLogRecordProcessor: vi.fn().mockImplementation(() => ({})), })) // Mock span processor vi.mock('@opentelemetry/sdk-trace-base', () => ({ - BatchSpanProcessor: vi.fn().mockImplementation(() => ({})), + SimpleSpanProcessor: vi.fn().mockImplementation(() => ({ + forceFlush: vi.fn().mockResolvedValue(undefined), + onStart: vi.fn(), + onEnd: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + })), + BatchSpanProcessor: vi.fn().mockImplementation(() => ({ + forceFlush: vi.fn().mockResolvedValue(undefined), + onStart: vi.fn(), + onEnd: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + })), })) -// Mock exporters -vi.mock('../src/telemetry-system/exporters', () => ({ +// Mock exporters to avoid real exporter instantiation +vi.mock('../../observability/src/telemetry-system/exporters', () => ({ EngineSpanExporter: vi.fn().mockImplementation(() => ({})), EngineMetricsExporter: vi.fn().mockImplementation(() => ({})), EngineLogExporter: vi.fn().mockImplementation(() => ({})), })) -// Mock the shared connection -vi.mock('../src/telemetry-system/connection', () => ({ +// Mock the shared connection to avoid real WebSocket connections +vi.mock('../../observability/src/telemetry-system/connection', () => ({ SharedEngineConnection: vi.fn().mockImplementation(() => ({ send: vi.fn(), onConnected: vi.fn(), @@ -69,7 +84,7 @@ vi.mock('../src/telemetry-system/connection', () => ({ })), })) -import { initOtel, shutdownOtel, getTracer } from '../src/telemetry-system/index' +import { initOtel, shutdownOtel, getTracer } from '@iii-dev/observability' describe('Fetch instrumentation', () => { const originalEnv = process.env @@ -195,7 +210,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -244,7 +259,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -284,7 +299,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -318,7 +333,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -353,7 +368,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -387,7 +402,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -419,7 +434,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -455,7 +470,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -487,7 +502,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -532,7 +547,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -571,7 +586,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch @@ -606,7 +621,7 @@ describe('Fetch span attributes', () => { } const { patchGlobalFetch, unpatchGlobalFetch } = await import( - '../src/telemetry-system/fetch-instrumentation' + '@iii-dev/observability' ) unpatch = unpatchGlobalFetch diff --git a/sdk/packages/node/iii/tests/logger-runtime.test.ts b/sdk/packages/node/iii/tests/logger-runtime.test.ts index c1dbddf893..0de3048890 100644 --- a/sdk/packages/node/iii/tests/logger-runtime.test.ts +++ b/sdk/packages/node/iii/tests/logger-runtime.test.ts @@ -1,5 +1,4 @@ -import { beforeAll, beforeEach, expect, it, vi } from 'vitest' -import { iii } from './utils' +import { beforeEach, expect, it, vi } from 'vitest' const emit = vi.fn() @@ -13,44 +12,58 @@ vi.mock('ws', () => { return { WebSocket: MockWebSocket, default: { WebSocket: MockWebSocket } } }) -vi.mock('../src/telemetry-system', async () => { - const actual = await vi.importActual( - '../src/telemetry-system', - ) +vi.mock('@iii-dev/observability', async (importOriginal) => { + const mod = await importOriginal() + + // Patch Logger.prototype so every Logger instance routes its otelLogger calls + // through our spy. See logger.test.ts for the full explanation of why this is + // needed instead of mocking getLogger at the package boundary. + Object.defineProperty(mod.Logger.prototype, 'otelLogger', { + get() { + return { emit } + }, + configurable: true, + }) return { - ...actual, + ...mod, + // Return null tracer so iii.ts falls back to the synthetic-span path when + // OTel is disabled, which is the behaviour this test exercises. getTracer: () => null, - getLogger: () => ({ emit }), - initOtel: vi.fn(), - shutdownOtel: vi.fn(), } }) -beforeAll(() => { - vi.spyOn(iii, 'shutdown').mockResolvedValue(undefined) -}) - beforeEach(() => emit.mockReset()) it('keeps an active span context for handlers when tracer setup is disabled', async () => { vi.resetModules() const { registerWorker, Logger } = await import('../src/index') - const sdk = registerWorker('ws://example.test', { otel: { enabled: false } }) as any + const { initOtel, shutdownOtel } = await import('@iii-dev/observability') - sdk.registerFunction('demo.handler', async () => { - new Logger().info('inside handler') - return { ok: true } - }) + // Register the AsyncLocalStorage context manager so that context.with + // (used by the synthetic-span code path in iii.ts) correctly propagates + // the span into the handler's execution context. + initOtel({ enabled: true, engineWsUrl: 'ws://localhost:49199', serviceName: 'test' }) + + try { + const sdk = registerWorker('ws://example.test', { otel: { enabled: false } }) as any - await sdk.functions.get('demo.handler').handler({}) + sdk.registerFunction('demo::handler', async () => { + new Logger().info('inside handler') + return { ok: true } + }) - expect(emit).toHaveBeenCalledWith( - expect.objectContaining({ - attributes: expect.objectContaining({ - trace_id: expect.any(String), - span_id: expect.any(String), + await sdk.functions.get('demo::handler').handler({}) + + expect(emit).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + trace_id: expect.any(String), + span_id: expect.any(String), + }), }), - }), - ) + ) + } finally { + await shutdownOtel() + } }) diff --git a/sdk/packages/node/iii/tests/logger.test.ts b/sdk/packages/node/iii/tests/logger.test.ts index ba33743758..0e8894b2fe 100644 --- a/sdk/packages/node/iii/tests/logger.test.ts +++ b/sdk/packages/node/iii/tests/logger.test.ts @@ -1,50 +1,61 @@ -import { context, trace } from '@opentelemetry/api' -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { iii } from './utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' const emit = vi.fn() -vi.mock('../src/telemetry-system', async () => { - const actual = await vi.importActual( - '../src/telemetry-system', - ) +vi.mock('@iii-dev/observability', async (importOriginal) => { + const mod = await importOriginal() + + // Patch Logger.prototype so every Logger instance routes its otelLogger calls + // through our spy. The mock replaces the package boundary but Logger's internal + // binding to getLogger (a relative import inside logger.ts) cannot be intercepted + // via package-level mocking — patching the prototype is the reliable alternative. + Object.defineProperty(mod.Logger.prototype, 'otelLogger', { + get() { + return { emit } + }, + configurable: true, + }) - return { - ...actual, - getLogger: () => ({ emit }), - } + return { ...mod } }) describe('Logger', () => { - beforeAll(() => { - vi.spyOn(iii, 'shutdown').mockResolvedValue(undefined) - }) - beforeEach(() => emit.mockReset()) it('uses the active span when no explicit trace ids are provided', async () => { - vi.resetModules() - const { Logger } = await import('../src/logger') - - const span = trace.wrapSpanContext({ - traceId: '11111111111111111111111111111111', - spanId: '2222222222222222', - traceFlags: 1, - }) - - await context.with(trace.setSpan(context.active(), span), async () => { - new Logger(undefined, 'orders-service').info('hello', { ok: true }) - }) - - expect(emit).toHaveBeenCalledWith( - expect.objectContaining({ - body: 'hello', - attributes: expect.objectContaining({ - trace_id: '11111111111111111111111111111111', - span_id: '2222222222222222', - 'service.name': 'orders-service', + // Importing dynamically (not at static import time) ensures the OTel + // context manager registered by setup.ts is visible in this execution context. + const { Logger, initOtel, shutdownOtel } = await import('@iii-dev/observability') + const { context, trace } = await import('@opentelemetry/api') + + // Ensure OTel context manager is registered so context.with propagates spans. + // vi.mock('@iii-dev/observability') prevents the setup.ts-triggered initOtel + // from registering in the current test context, so we call it explicitly here. + initOtel({ enabled: true, engineWsUrl: 'ws://localhost:49199', serviceName: 'test' }) + + try { + const span = trace.wrapSpanContext({ + traceId: '11111111111111111111111111111111', + spanId: '2222222222222222', + traceFlags: 1, + }) + + await context.with(trace.setSpan(context.active(), span), async () => { + new Logger(undefined, 'orders-service').info('hello', { ok: true }) + }) + + expect(emit).toHaveBeenCalledWith( + expect.objectContaining({ + body: 'hello', + attributes: expect.objectContaining({ + trace_id: '11111111111111111111111111111111', + span_id: '2222222222222222', + 'service.name': 'orders-service', + }), }), - }), - ) + ) + } finally { + await shutdownOtel() + } }) }) diff --git a/sdk/packages/node/iii/tests/otel-defaults.test.ts b/sdk/packages/node/iii/tests/otel-defaults.test.ts index 2ebf9af9ed..dbc1542912 100644 --- a/sdk/packages/node/iii/tests/otel-defaults.test.ts +++ b/sdk/packages/node/iii/tests/otel-defaults.test.ts @@ -22,6 +22,7 @@ vi.mock('ws', () => { vi.mock('@opentelemetry/sdk-trace-node', () => ({ NodeTracerProvider: vi.fn().mockImplementation(() => ({ register: vi.fn(), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), })) @@ -30,6 +31,7 @@ vi.mock('@opentelemetry/sdk-trace-node', () => ({ vi.mock('@opentelemetry/sdk-metrics', () => ({ MeterProvider: vi.fn().mockImplementation(() => ({ getMeter: vi.fn().mockReturnValue({ createCounter: vi.fn() }), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), PeriodicExportingMetricReader: vi.fn().mockImplementation(() => ({})), @@ -40,25 +42,38 @@ vi.mock('@opentelemetry/sdk-logs', () => ({ LoggerProvider: vi.fn().mockImplementation(() => ({ getLogger: vi.fn().mockReturnValue({ emit: vi.fn() }), addLogRecordProcessor: vi.fn(), + forceFlush: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), })), SimpleLogRecordProcessor: vi.fn().mockImplementation(() => ({})), + BatchLogRecordProcessor: vi.fn().mockImplementation(() => ({})), })) // Mock span processor vi.mock('@opentelemetry/sdk-trace-base', () => ({ - SimpleSpanProcessor: vi.fn().mockImplementation(() => ({})), + SimpleSpanProcessor: vi.fn().mockImplementation(() => ({ + forceFlush: vi.fn().mockResolvedValue(undefined), + onStart: vi.fn(), + onEnd: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + })), + BatchSpanProcessor: vi.fn().mockImplementation(() => ({ + forceFlush: vi.fn().mockResolvedValue(undefined), + onStart: vi.fn(), + onEnd: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + })), })) // Mock exporters to avoid real exporter instantiation -vi.mock('../src/telemetry-system/exporters', () => ({ +vi.mock('../../observability/src/telemetry-system/exporters', () => ({ EngineSpanExporter: vi.fn().mockImplementation(() => ({})), EngineMetricsExporter: vi.fn().mockImplementation(() => ({})), EngineLogExporter: vi.fn().mockImplementation(() => ({})), })) // Mock the shared connection to avoid real WebSocket connections -vi.mock('../src/telemetry-system/connection', () => ({ +vi.mock('../../observability/src/telemetry-system/connection', () => ({ SharedEngineConnection: vi.fn().mockImplementation(() => ({ send: vi.fn(), onConnected: vi.fn(), @@ -73,7 +88,7 @@ import { getTracer, getMeter, getLogger, -} from '../src/telemetry-system/index' +} from '@iii-dev/observability' describe('OTel default-enabled behavior', () => { const originalEnv = process.env diff --git a/sdk/packages/node/iii/tests/otel-worker-gauges.test.ts b/sdk/packages/node/iii/tests/otel-worker-gauges.test.ts index 52740bfd01..3fd5e43757 100644 --- a/sdk/packages/node/iii/tests/otel-worker-gauges.test.ts +++ b/sdk/packages/node/iii/tests/otel-worker-gauges.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { WorkerMetricsCollector } from '../src/worker-metrics' -import { registerWorkerGauges, stopWorkerGauges } from '../src/otel-worker-gauges' +import { WorkerMetricsCollector, registerWorkerGauges, stopWorkerGauges } from '@iii-dev/observability' type FakeGauge = { name: string } diff --git a/sdk/packages/node/iii/tests/payload.test.ts b/sdk/packages/node/iii/tests/payload.test.ts index ac91fecc97..44636fb878 100644 --- a/sdk/packages/node/iii/tests/payload.test.ts +++ b/sdk/packages/node/iii/tests/payload.test.ts @@ -4,7 +4,7 @@ import { redactAndTruncate, REDACTED_PLACEHOLDER, resolveMaxBytesFromEnv, -} from '../src/telemetry-system/payload' +} from '@iii-dev/observability' describe('redact', () => { it('redacts top-level sensitive keys', () => { diff --git a/sdk/packages/node/iii/tests/span-ops.test.ts b/sdk/packages/node/iii/tests/span-ops.test.ts index 1938142fe9..14bc406381 100644 --- a/sdk/packages/node/iii/tests/span-ops.test.ts +++ b/sdk/packages/node/iii/tests/span-ops.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, beforeAll, afterEach } from 'vitest' import { context, trace } from '@opentelemetry/api' import { BasicTracerProvider, @@ -11,13 +11,30 @@ import { recordSpanEvent, setCurrentSpanAttribute, setCurrentSpanError, -} from '../src/telemetry-system/span-ops' +} from '@iii-dev/observability' +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks' + +let activeProvider: BasicTracerProvider | null = null + +beforeAll(() => { + const cm = new AsyncLocalStorageContextManager() + cm.enable() + context.setGlobalContextManager(cm) +}) + +afterEach(async () => { + if (activeProvider) { + await activeProvider.shutdown() + activeProvider = null + } +}) function buildTestProvider() { const exporter = new InMemorySpanExporter() const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)], }) + activeProvider = provider const tracer = provider.getTracer('test') return { tracer, exporter, provider } } diff --git a/sdk/packages/node/iii/tests/worker-metrics.test.ts b/sdk/packages/node/iii/tests/worker-metrics.test.ts index 1bed8e83fa..b0f3697733 100644 --- a/sdk/packages/node/iii/tests/worker-metrics.test.ts +++ b/sdk/packages/node/iii/tests/worker-metrics.test.ts @@ -1,6 +1,6 @@ import { performance } from 'node:perf_hooks' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { WorkerMetricsCollector } from '../src/worker-metrics' +import { WorkerMetricsCollector } from '@iii-dev/observability' type MutableCollectorInternals = { lastCpuUsage: NodeJS.CpuUsage diff --git a/sdk/packages/node/iii/tsdown.config.ts b/sdk/packages/node/iii/tsdown.config.ts index d68f9bfe79..af7323df99 100644 --- a/sdk/packages/node/iii/tsdown.config.ts +++ b/sdk/packages/node/iii/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsdown' export default defineConfig({ - entry: ['./src/index.ts', './src/stream.ts', './src/state.ts', './src/telemetry.ts'], + entry: ['./src/index.ts', './src/stream.ts', './src/state.ts'], format: ['esm', 'cjs'], dts: true, sourcemap: true, diff --git a/sdk/packages/node/iii/vitest.config.ts b/sdk/packages/node/iii/vitest.config.ts index 59588dfc2b..1b3b670e74 100644 --- a/sdk/packages/node/iii/vitest.config.ts +++ b/sdk/packages/node/iii/vitest.config.ts @@ -1,10 +1,19 @@ +import path from 'path' +import { fileURLToPath } from 'url' import { defineConfig } from 'vitest/config' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ + resolve: { + alias: { + '@iii-dev/observability': path.resolve(__dirname, '../observability/src/index.ts'), + }, + }, test: { globals: true, testTimeout: 30000, - hookTimeout: 30000, + hookTimeout: 60000, setupFiles: ['./tests/setup.ts'], coverage: { provider: 'v8', diff --git a/sdk/packages/node/observability/README.md b/sdk/packages/node/observability/README.md new file mode 100644 index 0000000000..4522df81e7 --- /dev/null +++ b/sdk/packages/node/observability/README.md @@ -0,0 +1,5 @@ +# @iii-dev/observability + +OpenTelemetry + Logger primitives shared across the iii SDKs. + +See https://github.com/iii-hq/iii for the full project. diff --git a/sdk/packages/node/observability/package.json b/sdk/packages/node/observability/package.json new file mode 100644 index 0000000000..ba5e27527f --- /dev/null +++ b/sdk/packages/node/observability/package.json @@ -0,0 +1,48 @@ +{ + "name": "@iii-dev/observability", + "version": "0.13.0-next.1", + "private": false, + "publishConfig": { "access": "public" }, + "type": "module", + "author": "III", + "license": "Apache-2.0", + "description": "OpenTelemetry and logging primitives shared across iii SDKs.", + "keywords": ["iii", "observability", "opentelemetry", "logger"], + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": ["dist", "README.md"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsdown", + "test": "vitest run", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.57.0", + "@opentelemetry/core": "^1.30.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/otlp-transformer": "^0.57.0", + "@opentelemetry/resources": "^1.30.0", + "@opentelemetry/sdk-logs": "^0.57.0", + "@opentelemetry/sdk-metrics": "^1.30.0", + "@opentelemetry/sdk-trace-base": "^1.30.0", + "@opentelemetry/sdk-trace-node": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/ws": "^8.18.1", + "@vitest/coverage-v8": "^2.1.0", + "tsdown": "^0.21.4", + "typescript": "^5.9.3", + "vitest": "^2.1.0" + } +} diff --git a/sdk/packages/node/observability/src/http-instrumentation.test.ts b/sdk/packages/node/observability/src/http-instrumentation.test.ts new file mode 100644 index 0000000000..0a441d5363 --- /dev/null +++ b/sdk/packages/node/observability/src/http-instrumentation.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it, vi } from 'vitest' +import { executeTracedRequest } from './http-instrumentation' + +describe('executeTracedRequest', () => { + it('forwards to fetch and returns the response', async () => { + const originalFetch = globalThis.fetch + globalThis.fetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 })) + try { + const res = await executeTracedRequest('https://example.com/api') + expect(res.status).toBe(200) + expect(await res.text()).toBe('ok') + } finally { + globalThis.fetch = originalFetch + } + }) +}) diff --git a/sdk/packages/node/observability/src/http-instrumentation.ts b/sdk/packages/node/observability/src/http-instrumentation.ts new file mode 100644 index 0000000000..e0bc37806e --- /dev/null +++ b/sdk/packages/node/observability/src/http-instrumentation.ts @@ -0,0 +1,97 @@ +import { context as otelContext, propagation, SpanKind, SpanStatusCode, trace, type Tracer } from '@opentelemetry/api' + +const SAFE_REQUEST_HEADERS = ['content-type', 'accept'] as const +const SAFE_RESPONSE_HEADERS = ['content-type'] as const + +export interface TracedFetchInit extends RequestInit { + tracer?: Tracer +} + +/** + * Execute a fetch request inside an OTel CLIENT span. + * + * Mirrors the Rust execute_traced_request shape: injects W3C traceparent into + * outgoing headers, records HTTP semantic-convention attributes, and sets + * ERROR span status for HTTP responses with status >= 400 or network errors. + */ +export async function executeTracedRequest( + input: RequestInfo | URL, + init?: TracedFetchInit, +): Promise { + const tracer = init?.tracer ?? trace.getTracer('iii-node-sdk') + const rawUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url + let url: URL | null + try { + url = new URL(rawUrl) + } catch { + url = null + } + const method = (init?.method ?? (typeof input === 'object' && 'method' in input ? input.method : 'GET') ?? 'GET').toUpperCase() + const name = url?.pathname ? `${method} ${url.pathname}` : method + + return tracer.startActiveSpan( + name, + { + kind: SpanKind.CLIENT, + attributes: { + 'http.request.method': method, + 'url.full': url?.toString() ?? rawUrl, + 'network.protocol.name': 'http', + ...(url + ? { + 'server.address': url.hostname, + 'url.scheme': url.protocol.replace(':', ''), + 'url.path': url.pathname, + } + : {}), + ...(url?.port ? { 'server.port': Number(url.port) } : {}), + ...(url?.search ? { 'url.query': url.search.slice(1) } : {}), + }, + }, + async (span) => { + try { + // Seed with the Request's own headers so they survive when caller + // passes a Request object; init.headers (if any) then overrides. + const baseHeaders = + typeof input === 'object' && 'headers' in input ? input.headers : undefined + const headers = new Headers(baseHeaders) + if (init?.headers) { + for (const [k, v] of new Headers(init.headers).entries()) headers.set(k, v) + } + const carrier: Record = {} + propagation.inject(otelContext.active(), carrier) + for (const [k, v] of Object.entries(carrier)) headers.set(k, v) + + for (const h of SAFE_REQUEST_HEADERS) { + const v = headers.get(h) + if (v) span.setAttribute(`http.request.header.${h}`, v) + } + + const response = await fetch(input, { ...init, headers }) + span.setAttribute('http.response.status_code', response.status) + const cl = response.headers.get('content-length') + if (cl) span.setAttribute('http.response.body.size', Number(cl)) + for (const h of SAFE_RESPONSE_HEADERS) { + const v = response.headers.get(h) + if (v) span.setAttribute(`http.response.header.${h}`, v) + } + + if (response.status >= 400) { + span.setStatus({ code: SpanStatusCode.ERROR, message: String(response.status) }) + span.setAttribute('error.type', String(response.status)) + } else { + span.setStatus({ code: SpanStatusCode.OK }) + } + return response + } catch (err) { + const error = err as Error + span.recordException(error) + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }) + span.setAttribute('error.type', error.name) + throw err + } finally { + span.end() + } + }, + ) +} diff --git a/sdk/packages/node/iii/src/telemetry.ts b/sdk/packages/node/observability/src/index.ts similarity index 57% rename from sdk/packages/node/iii/src/telemetry.ts rename to sdk/packages/node/observability/src/index.ts index 3376bb07e0..b606e9a0de 100644 --- a/sdk/packages/node/iii/src/telemetry.ts +++ b/sdk/packages/node/observability/src/index.ts @@ -1,52 +1,49 @@ -export { - DEFAULT_BRIDGE_RECONNECTION_CONFIG, - DEFAULT_INVOCATION_TIMEOUT_MS, - EngineFunctions, - EngineTriggers, - type IIIConnectionState, - type IIIReconnectionConfig, - LogFunctions, -} from './iii-constants' -export type { RegisterFunctionFormat } from './iii-types' -export { - registerWorkerGauges, - stopWorkerGauges, - type WorkerGaugesOptions, -} from './otel-worker-gauges' +export { Logger } from './logger' +export { executeTracedRequest } from './http-instrumentation' +export type { TracedFetchInit } from './http-instrumentation' export { BaggageSpanProcessor, + DEFAULT_ALLOWLIST, currentSpanId, currentSpanIsRecording, currentTraceId, - DEFAULT_ALLOWLIST, extractBaggage, extractContext, extractTraceparent, + flushOtel, getAllBaggage, getBaggageEntry, getLogger, + getMeter, + getTracer, initOtel, injectBaggage, injectTraceparent, - type Logger as OtelLogger, - type Meter, - type OtelConfig, - type ReconnectionConfig, + recordSpanEvent, redact, redactAndTruncate, REDACTED_PLACEHOLDER, - resolveMaxBytesFromEnv, - recordSpanEvent, removeBaggageEntry, - SeverityNumber, + resolveMaxBytesFromEnv, + setBaggageEntry, setCurrentSpanAttribute, setCurrentSpanError, - type Span, - setBaggageEntry, + SeverityNumber, shutdownOtel, + SpanKind, withSpan, } from './telemetry-system' +export type { + OtelApiLogger as OtelLogger, + Meter, + OtelConfig, + ReconnectionConfig, + Span, +} from './telemetry-system' +export { patchGlobalFetch, unpatchGlobalFetch } from './telemetry-system/fetch-instrumentation' +export { registerWorkerGauges, stopWorkerGauges } from './otel-worker-gauges' +export type { WorkerGaugesOptions } from './otel-worker-gauges' +export { WorkerMetricsCollector } from './worker-metrics' +export type { WorkerMetrics, WorkerMetricsCollectorOptions } from './worker-metrics' export type { OtelLogEvent } from './types' export { safeStringify } from './utils' -export type { WorkerMetrics, WorkerMetricsCollectorOptions } from './worker-metrics' -export { WorkerMetricsCollector } from './worker-metrics' diff --git a/sdk/packages/node/observability/src/logger.test.ts b/sdk/packages/node/observability/src/logger.test.ts new file mode 100644 index 0000000000..7f83ca73a1 --- /dev/null +++ b/sdk/packages/node/observability/src/logger.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { Logger } from './logger' + +describe('Logger', () => { + it('exposes info, warn, error, and debug methods', () => { + const log = new Logger() + expect(typeof log.info).toBe('function') + expect(typeof log.warn).toBe('function') + expect(typeof log.error).toBe('function') + expect(typeof log.debug).toBe('function') + }) +}) diff --git a/sdk/packages/node/iii/src/logger.ts b/sdk/packages/node/observability/src/logger.ts similarity index 100% rename from sdk/packages/node/iii/src/logger.ts rename to sdk/packages/node/observability/src/logger.ts diff --git a/sdk/packages/node/iii/src/otel-worker-gauges.ts b/sdk/packages/node/observability/src/otel-worker-gauges.ts similarity index 100% rename from sdk/packages/node/iii/src/otel-worker-gauges.ts rename to sdk/packages/node/observability/src/otel-worker-gauges.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/baggage-span-processor.ts b/sdk/packages/node/observability/src/telemetry-system/baggage-span-processor.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/baggage-span-processor.ts rename to sdk/packages/node/observability/src/telemetry-system/baggage-span-processor.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/connection.ts b/sdk/packages/node/observability/src/telemetry-system/connection.ts similarity index 88% rename from sdk/packages/node/iii/src/telemetry-system/connection.ts rename to sdk/packages/node/observability/src/telemetry-system/connection.ts index b0bd76a2e5..1e9ff45e60 100644 --- a/sdk/packages/node/iii/src/telemetry-system/connection.ts +++ b/sdk/packages/node/observability/src/telemetry-system/connection.ts @@ -21,6 +21,7 @@ export class SharedEngineConnection { private config: ReconnectionConfig private state: ConnectionState = 'disconnected' private onConnectedCallbacks: Array<() => void> = [] + private onFailedCallbacks: Array<() => void> = [] constructor(wsUrl: string, config: Partial = {}) { this.wsUrl = wsUrl @@ -112,6 +113,15 @@ export class SharedEngineConnection { for (const { callback } of pending) { callback?.(failedError) } + + // Notify failed callbacks so exporters can drain their own queues + for (const cb of this.onFailedCallbacks) { + try { + cb() + } catch (err) { + console.error('[OTel] onFailed callback threw:', err) + } + } return } @@ -166,6 +176,22 @@ export class SharedEngineConnection { } } + /** + * Register a callback to be called when the connection enters the failed + * terminal state (max retries reached). Exporters use this to drain their + * own pending queues so in-flight forceFlush() calls do not hang. + */ + onFailed(callback: () => void): void { + this.onFailedCallbacks.push(callback) + if (this.state === 'failed') { + try { + callback() + } catch (err) { + console.error('[OTel] onFailed callback threw:', err) + } + } + } + /** * Get the current connection state. */ @@ -197,6 +223,7 @@ export class SharedEngineConnection { callback?.(shutdownError) } this.onConnectedCallbacks = [] + this.onFailedCallbacks = [] this.state = 'disconnected' } } diff --git a/sdk/packages/node/observability/src/telemetry-system/context.test.ts b/sdk/packages/node/observability/src/telemetry-system/context.test.ts new file mode 100644 index 0000000000..d30c73b290 --- /dev/null +++ b/sdk/packages/node/observability/src/telemetry-system/context.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest' +import { currentSpanId, currentTraceId } from './context' + +describe('context', () => { + it('returns undefined outside any span', () => { + expect(currentSpanId()).toBeUndefined() + expect(currentTraceId()).toBeUndefined() + }) +}) diff --git a/sdk/packages/node/iii/src/telemetry-system/context.ts b/sdk/packages/node/observability/src/telemetry-system/context.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/context.ts rename to sdk/packages/node/observability/src/telemetry-system/context.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/exporters.ts b/sdk/packages/node/observability/src/telemetry-system/exporters.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/exporters.ts rename to sdk/packages/node/observability/src/telemetry-system/exporters.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/fetch-instrumentation.ts b/sdk/packages/node/observability/src/telemetry-system/fetch-instrumentation.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/fetch-instrumentation.ts rename to sdk/packages/node/observability/src/telemetry-system/fetch-instrumentation.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/index.ts b/sdk/packages/node/observability/src/telemetry-system/index.ts similarity index 95% rename from sdk/packages/node/iii/src/telemetry-system/index.ts rename to sdk/packages/node/observability/src/telemetry-system/index.ts index 57570a096e..a5004f9e73 100644 --- a/sdk/packages/node/iii/src/telemetry-system/index.ts +++ b/sdk/packages/node/observability/src/telemetry-system/index.ts @@ -240,6 +240,23 @@ export async function shutdownOtel(): Promise { logger = null } +/** + * Force-flush all OTel providers without tearing them down. + * + * Counterpart to {@link shutdownOtel}. Use before short-lived process exits + * where you want pending spans/metrics/logs delivered but plan to keep using + * OTel afterwards. + */ +export async function flushOtel(): Promise { + await Promise.all( + [ + tracerProvider?.forceFlush(), + meterProvider?.forceFlush(), + loggerProvider?.forceFlush(), + ].filter(Boolean), + ) +} + /** * Get the OpenTelemetry tracer instance. */ @@ -306,4 +323,4 @@ export async function withSpan( } // Re-export OTEL types for convenience -export { SpanKind, SpanStatusCode, SeverityNumber, type Span, type Context, type Tracer, type Meter, type Logger } +export { SpanKind, SpanStatusCode, SeverityNumber, type Span, type Context, type Tracer, type Meter, type Logger as OtelApiLogger } diff --git a/sdk/packages/node/iii/src/telemetry-system/log-exporter.ts b/sdk/packages/node/observability/src/telemetry-system/log-exporter.ts similarity index 68% rename from sdk/packages/node/iii/src/telemetry-system/log-exporter.ts rename to sdk/packages/node/observability/src/telemetry-system/log-exporter.ts index 0a65869faa..73b6217673 100644 --- a/sdk/packages/node/iii/src/telemetry-system/log-exporter.ts +++ b/sdk/packages/node/observability/src/telemetry-system/log-exporter.ts @@ -13,6 +13,7 @@ import { PREFIX_LOGS } from './types' * Log exporter using the shared WebSocket connection. */ export class EngineLogExporter implements LogRecordExporter { + private static readonly MAX_PENDING_EXPORTS = 100 private connection: SharedEngineConnection private pendingExports: Array<{ logs: ReadableLogRecord[] @@ -22,6 +23,7 @@ export class EngineLogExporter implements LogRecordExporter { constructor(connection: SharedEngineConnection) { this.connection = connection this.connection.onConnected(() => this.flushPending()) + this.connection.onFailed(() => this.failPending()) } private flushPending(): void { @@ -31,11 +33,35 @@ export class EngineLogExporter implements LogRecordExporter { } } + private failPending(): void { + const pending = this.pendingExports.splice(0, this.pendingExports.length) + const error = new Error('Connection failed: dropping queued logs') + for (const { callback } of pending) { + callback({ code: ExportResultCode.FAILED, error }) + } + } + private doExport( logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void, ): void { - if (this.connection.getState() !== 'connected') { + const state = this.connection.getState() + if (state === 'failed') { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Connection failed: dropping logs'), + }) + return + } + if (state !== 'connected') { + if (this.pendingExports.length >= EngineLogExporter.MAX_PENDING_EXPORTS) { + const dropped = this.pendingExports.shift() + dropped?.callback({ + code: ExportResultCode.FAILED, + error: new Error('Logs export queue full'), + }) + console.warn('[OTel] Logs export queue full, dropped oldest entry') + } this.pendingExports.push({ logs, callback: resultCallback }) return } @@ -65,6 +91,8 @@ export class EngineLogExporter implements LogRecordExporter { this.doExport(logs, resultCallback) } + async forceFlush(): Promise {} + async shutdown(): Promise { for (const { callback } of this.pendingExports) { callback({ code: ExportResultCode.FAILED, error: new Error('Exporter shutdown') }) diff --git a/sdk/packages/node/iii/src/telemetry-system/metrics-exporter.ts b/sdk/packages/node/observability/src/telemetry-system/metrics-exporter.ts similarity index 83% rename from sdk/packages/node/iii/src/telemetry-system/metrics-exporter.ts rename to sdk/packages/node/observability/src/telemetry-system/metrics-exporter.ts index 27ea20a4ee..d279fe21f3 100644 --- a/sdk/packages/node/iii/src/telemetry-system/metrics-exporter.ts +++ b/sdk/packages/node/observability/src/telemetry-system/metrics-exporter.ts @@ -23,6 +23,7 @@ export class EngineMetricsExporter implements PushMetricExporter { constructor(connection: SharedEngineConnection) { this.connection = connection this.connection.onConnected(() => this.flushPending()) + this.connection.onFailed(() => this.failPending()) } private flushPending(): void { @@ -32,6 +33,14 @@ export class EngineMetricsExporter implements PushMetricExporter { } } + private failPending(): void { + const pending = this.pendingExports.splice(0, this.pendingExports.length) + const error = new Error('Connection failed: dropping queued metrics') + for (const { resultCallback } of pending) { + resultCallback?.({ code: ExportResultCode.FAILED, error }) + } + } + private sendExport( metricsData: ResourceMetrics, resultCallback?: (result: ExportResult) => void, @@ -61,7 +70,15 @@ export class EngineMetricsExporter implements PushMetricExporter { metricsData: ResourceMetrics, resultCallback: (result: ExportResult) => void, ): void { - if (this.connection.getState() !== 'connected') { + const state = this.connection.getState() + if (state === 'failed') { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Connection failed: dropping metrics'), + }) + return + } + if (state !== 'connected') { if (this.pendingExports.length >= EngineMetricsExporter.MAX_PENDING_EXPORTS) { const dropped = this.pendingExports.shift() dropped?.resultCallback?.({ diff --git a/sdk/packages/node/iii/src/telemetry-system/payload.ts b/sdk/packages/node/observability/src/telemetry-system/payload.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/payload.ts rename to sdk/packages/node/observability/src/telemetry-system/payload.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/span-exporter.ts b/sdk/packages/node/observability/src/telemetry-system/span-exporter.ts similarity index 83% rename from sdk/packages/node/iii/src/telemetry-system/span-exporter.ts rename to sdk/packages/node/observability/src/telemetry-system/span-exporter.ts index 51e2ef97a4..15e308f28c 100644 --- a/sdk/packages/node/iii/src/telemetry-system/span-exporter.ts +++ b/sdk/packages/node/observability/src/telemetry-system/span-exporter.ts @@ -23,6 +23,7 @@ export class EngineSpanExporter implements SpanExporter { constructor(connection: SharedEngineConnection) { this.connection = connection this.connection.onConnected(() => this.flushPending()) + this.connection.onFailed(() => this.failPending()) } private flushPending(): void { @@ -32,6 +33,14 @@ export class EngineSpanExporter implements SpanExporter { } } + private failPending(): void { + const pending = this.pendingExports.splice(0, this.pendingExports.length) + const error = new Error('Connection failed: dropping queued spans') + for (const { resultCallback } of pending) { + resultCallback?.({ code: ExportResultCode.FAILED, error }) + } + } + private sendExport(spans: ReadableSpan[], resultCallback?: (result: ExportResult) => void): void { try { const serialized = JsonTraceSerializer.serializeRequest(spans) @@ -55,7 +64,15 @@ export class EngineSpanExporter implements SpanExporter { } private doExport(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void { - if (this.connection.getState() !== 'connected') { + const state = this.connection.getState() + if (state === 'failed') { + resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Connection failed: dropping spans'), + }) + return + } + if (state !== 'connected') { if (this.pendingExports.length >= EngineSpanExporter.MAX_PENDING_EXPORTS) { const dropped = this.pendingExports.shift() dropped?.resultCallback?.({ diff --git a/sdk/packages/node/iii/src/telemetry-system/span-ops.ts b/sdk/packages/node/observability/src/telemetry-system/span-ops.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/span-ops.ts rename to sdk/packages/node/observability/src/telemetry-system/span-ops.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/types.ts b/sdk/packages/node/observability/src/telemetry-system/types.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/types.ts rename to sdk/packages/node/observability/src/telemetry-system/types.ts diff --git a/sdk/packages/node/iii/src/telemetry-system/utils.ts b/sdk/packages/node/observability/src/telemetry-system/utils.ts similarity index 100% rename from sdk/packages/node/iii/src/telemetry-system/utils.ts rename to sdk/packages/node/observability/src/telemetry-system/utils.ts diff --git a/sdk/packages/node/observability/src/types.ts b/sdk/packages/node/observability/src/types.ts new file mode 100644 index 0000000000..6d51c5b597 --- /dev/null +++ b/sdk/packages/node/observability/src/types.ts @@ -0,0 +1,27 @@ +/** OTEL Log Event from the engine */ +export type OtelLogEvent = { + /** Timestamp in Unix nanoseconds */ + timestamp_unix_nano: number + /** Observed timestamp in Unix nanoseconds */ + observed_timestamp_unix_nano: number + /** OTEL severity number (1-24): TRACE=1-4, DEBUG=5-8, INFO=9-12, WARN=13-16, ERROR=17-20, FATAL=21-24 */ + severity_number: number + /** Severity text (e.g., "INFO", "WARN", "ERROR") */ + severity_text: string + /** Log message body */ + body: string + /** Structured attributes */ + attributes: Record + /** Trace ID for correlation (if available) */ + trace_id?: string + /** Span ID for correlation (if available) */ + span_id?: string + /** Resource attributes from the emitting service */ + resource: Record + /** Service name that emitted the log */ + service_name: string + /** Instrumentation scope name (if available) */ + instrumentation_scope_name?: string + /** Instrumentation scope version (if available) */ + instrumentation_scope_version?: string +} diff --git a/sdk/packages/node/observability/src/utils.ts b/sdk/packages/node/observability/src/utils.ts new file mode 100644 index 0000000000..ef8d4eb2e3 --- /dev/null +++ b/sdk/packages/node/observability/src/utils.ts @@ -0,0 +1,29 @@ +/** + * Safely stringify a value, handling circular references, BigInt, and other edge cases. + * Returns "[unserializable]" if serialization fails for any reason. + */ +export function safeStringify(value: unknown): string { + const seen = new WeakSet() + + try { + const result = JSON.stringify(value, (_key, val) => { + // Handle BigInt + if (typeof val === 'bigint') { + return val.toString() + } + + // Handle circular references + if (val !== null && typeof val === 'object') { + if (seen.has(val)) { + return '[Circular]' + } + seen.add(val) + } + + return val + }) + return result ?? '[unserializable]' + } catch { + return '[unserializable]' + } +} diff --git a/sdk/packages/node/iii/src/worker-metrics.ts b/sdk/packages/node/observability/src/worker-metrics.ts similarity index 100% rename from sdk/packages/node/iii/src/worker-metrics.ts rename to sdk/packages/node/observability/src/worker-metrics.ts diff --git a/sdk/packages/node/observability/tsconfig.json b/sdk/packages/node/observability/tsconfig.json new file mode 100644 index 0000000000..1d676dec5a --- /dev/null +++ b/sdk/packages/node/observability/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/sdk/packages/node/observability/tsdown.config.ts b/sdk/packages/node/observability/tsdown.config.ts new file mode 100644 index 0000000000..7611c033fb --- /dev/null +++ b/sdk/packages/node/observability/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + minify: false, + treeshake: true, + deps: { neverBundle: [] }, +}) diff --git a/sdk/packages/node/observability/vitest.config.ts b/sdk/packages/node/observability/vitest.config.ts new file mode 100644 index 0000000000..9e1ac479b5 --- /dev/null +++ b/sdk/packages/node/observability/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { include: ['src/**/*.test.ts'] }, +}) diff --git a/sdk/packages/python/iii-example/pyproject.toml b/sdk/packages/python/iii-example/pyproject.toml index 41092985c8..056cade110 100644 --- a/sdk/packages/python/iii-example/pyproject.toml +++ b/sdk/packages/python/iii-example/pyproject.toml @@ -10,12 +10,14 @@ authors = [{ name = "III" }] requires-python = ">=3.10" dependencies = [ "iii-sdk", + "iii-observability", "opentelemetry-api", "opentelemetry-sdk", ] [tool.uv.sources] iii-sdk = { path = "../iii", editable = true } +iii-observability = { path = "../observability", editable = true } [project.scripts] iii-example = "src.main:main" diff --git a/sdk/packages/python/iii-example/src/hooks.py b/sdk/packages/python/iii-example/src/hooks.py index 98e230c155..865548517f 100644 --- a/sdk/packages/python/iii-example/src/hooks.py +++ b/sdk/packages/python/iii-example/src/hooks.py @@ -1,7 +1,8 @@ import uuid from typing import Any, Awaitable, Callable -from iii import ApiRequest, ApiResponse, IIIClient, Logger +from iii import ApiRequest, ApiResponse, IIIClient +from iii_observability import Logger def use_api( diff --git a/sdk/packages/python/iii-example/src/state.py b/sdk/packages/python/iii-example/src/state.py index fdf2f818a5..275bb1a57e 100644 --- a/sdk/packages/python/iii-example/src/state.py +++ b/sdk/packages/python/iii-example/src/state.py @@ -8,13 +8,13 @@ def __init__(self, iii: IIIClient) -> None: self._iii = iii async def get(self, scope: str, key: str) -> Any | None: - return await self._iii.trigger({"function_id": "state::get", "payload": {"scope": scope, "key": key}}) + return await self._iii.trigger_async({"function_id": "state::get", "payload": {"scope": scope, "key": key}}) async def set(self, scope: str, key: str, value: Any) -> Any: - return await self._iii.trigger({"function_id": "state::set", "payload": {"scope": scope, "key": key, "value": value}}) + return await self._iii.trigger_async({"function_id": "state::set", "payload": {"scope": scope, "key": key, "value": value}}) async def delete(self, scope: str, key: str) -> None: - return await self._iii.trigger({"function_id": "state::delete", "payload": {"scope": scope, "key": key}}) + return await self._iii.trigger_async({"function_id": "state::delete", "payload": {"scope": scope, "key": key}}) async def get_group(self, scope: str) -> list[Any]: - return await self._iii.trigger({"function_id": "state::list", "payload": {"scope": scope}}) + return await self._iii.trigger_async({"function_id": "state::list", "payload": {"scope": scope}}) diff --git a/sdk/packages/python/iii-example/src/stream.py b/sdk/packages/python/iii-example/src/stream.py index b2fada0e7b..e558f30230 100644 --- a/sdk/packages/python/iii-example/src/stream.py +++ b/sdk/packages/python/iii-example/src/stream.py @@ -22,31 +22,31 @@ def __init__(self, iii: IIIClient) -> None: self._iii = iii async def get(self, stream_name: str, group_id: str, item_id: str) -> Any | None: - return await self._iii.trigger({ + return await self._iii.trigger_async({ "function_id": "stream::get", "payload": {"stream_name": stream_name, "group_id": group_id, "item_id": item_id}, }) async def set(self, stream_name: str, group_id: str, item_id: str, data: Any) -> Any: - return await self._iii.trigger({ + return await self._iii.trigger_async({ "function_id": "stream::set", "payload": {"stream_name": stream_name, "group_id": group_id, "item_id": item_id, "data": data}, }) async def delete(self, stream_name: str, group_id: str, item_id: str) -> None: - return await self._iii.trigger({ + return await self._iii.trigger_async({ "function_id": "stream::delete", "payload": {"stream_name": stream_name, "group_id": group_id, "item_id": item_id}, }) async def get_group(self, stream_name: str, group_id: str) -> list[Any]: - return await self._iii.trigger({ + return await self._iii.trigger_async({ "function_id": "stream::list", "payload": {"stream_name": stream_name, "group_id": group_id}, }) async def list_groups(self, stream_name: str) -> list[str]: - return await self._iii.trigger({"function_id": "stream::list_groups", "payload": {"stream_name": stream_name}}) + return await self._iii.trigger_async({"function_id": "stream::list_groups", "payload": {"stream_name": stream_name}}) class TodoStream(IStream[dict[str, Any]]): diff --git a/sdk/packages/python/iii-example/uv.lock b/sdk/packages/python/iii-example/uv.lock index aeacb161ff..5241c4f725 100644 --- a/sdk/packages/python/iii-example/uv.lock +++ b/sdk/packages/python/iii-example/uv.lock @@ -11,11 +11,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.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" } +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" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + [[package]] name = "iii-example" version = "0.0.1" source = { editable = "." } dependencies = [ + { name = "iii-observability" }, { name = "iii-sdk" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -23,18 +105,45 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "iii-observability", editable = "../observability" }, { name = "iii-sdk", editable = "../iii" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, ] +[[package]] +name = "iii-observability" +version = "0.13.0.dev1" +source = { editable = "../observability" } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, + { name = "opentelemetry-api", specifier = ">=1.25" }, + { name = "opentelemetry-sdk", specifier = ">=1.25" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.30" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2" }, + { name = "websockets", specifier = ">=12.0" }, +] +provides-extras = ["dev"] + [[package]] name = "iii-sdk" -version = "0.11.7.dev1" +version = "0.13.0.dev1" source = { editable = "../iii" } dependencies = [ + { name = "iii-observability" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "websockets" }, ] @@ -44,9 +153,9 @@ requires-dist = [ { name = "aiohttp", marker = "extra == 'dev'", specifier = ">=3.9" }, { name = "anyio", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "griffe", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "iii-observability", editable = "../observability" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, { name = "opentelemetry-api", specifier = ">=1.25" }, - { name = "opentelemetry-sdk", specifier = ">=1.25" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, diff --git a/sdk/packages/python/iii/pyproject.toml b/sdk/packages/python/iii/pyproject.toml index af6d78e304..e77b8a80e9 100644 --- a/sdk/packages/python/iii/pyproject.toml +++ b/sdk/packages/python/iii/pyproject.toml @@ -23,13 +23,16 @@ dependencies = [ "websockets>=12.0", "pydantic>=2.0", "opentelemetry-api>=1.25", - "opentelemetry-sdk>=1.25", + "iii-observability==0.13.0.dev1", ] [project.urls] Homepage = "https://github.com/iii-hq/iii" Repository = "https://github.com/iii-hq/iii" +[tool.uv.sources] +iii-observability = { path = "../observability", editable = true } + [project.optional-dependencies] dev = [ "pytest>=8.0", diff --git a/sdk/packages/python/iii/src/iii/__init__.py b/sdk/packages/python/iii/src/iii/__init__.py index 5aa84f3282..bf6cdc1314 100644 --- a/sdk/packages/python/iii/src/iii/__init__.py +++ b/sdk/packages/python/iii/src/iii/__init__.py @@ -1,11 +1,37 @@ """III SDK for Python.""" -from .baggage_span_processor import DEFAULT_ALLOWLIST, BaggageSpanProcessor +from iii_observability import ( + DEFAULT_ALLOWLIST, + REDACTED_PLACEHOLDER, + BaggageSpanProcessor, + Logger, + OtelConfig, + ReconnectionConfig, + current_span_id, + current_span_is_recording, + current_trace_id, + execute_traced_request, + extract_baggage, + extract_traceparent, + flush_otel, + init_otel, + inject_baggage, + inject_traceparent, + record_span_event, + redact, + redact_and_truncate, + resolve_max_bytes_from_env, + set_current_span_attribute, + set_current_span_error, + shutdown_otel, + with_span, +) + from .channels import ChannelReader, ChannelWriter from .errors import IIIForbiddenError, IIIInvocationError, IIITimeoutError from .format_utils import extract_request_format, extract_response_format, python_type_to_format from .iii import TriggerAction, register_worker -from .iii_constants import FunctionRef, InitOptions, ReconnectionConfig, TelemetryOptions +from .iii_constants import FunctionRef, InitOptions, TelemetryOptions from .iii_types import ( AuthInput, AuthResult, @@ -31,19 +57,6 @@ TriggerActionVoid, TriggerRequest, ) -from .logger import Logger -from .payload import ( - REDACTED_PLACEHOLDER, - redact, - redact_and_truncate, - resolve_max_bytes_from_env, -) -from .span_ops import ( - current_span_is_recording, - record_span_event, - set_current_span_attribute, - set_current_span_error, -) from .stream import ( IStream, StreamChangeEvent, @@ -53,7 +66,6 @@ StreamJoinLeaveTriggerConfig, StreamTriggerConfig, ) -from .telemetry_types import OtelConfig from .triggers import Trigger, TriggerConfig, TriggerHandler, TriggerTypeRef from .types import ( ApiRequest, @@ -72,13 +84,24 @@ "BaggageSpanProcessor", "DEFAULT_ALLOWLIST", "REDACTED_PLACEHOLDER", + "current_span_id", "current_span_is_recording", + "current_trace_id", + "execute_traced_request", + "extract_baggage", + "extract_traceparent", + "flush_otel", + "init_otel", + "inject_baggage", + "inject_traceparent", "record_span_event", - "set_current_span_attribute", - "set_current_span_error", "redact", "redact_and_truncate", "resolve_max_bytes_from_env", + "set_current_span_attribute", + "set_current_span_error", + "shutdown_otel", + "with_span", # Channels "ChannelReader", "ChannelWriter", diff --git a/sdk/packages/python/iii/src/iii/iii.py b/sdk/packages/python/iii/src/iii/iii.py index 95f8297de4..9479f405dd 100644 --- a/sdk/packages/python/iii/src/iii/iii.py +++ b/sdk/packages/python/iii/src/iii/iii.py @@ -14,6 +14,7 @@ from typing import Any, Awaitable, Callable, Coroutine, TypeVar, cast import websockets +from iii_observability import OtelConfig from websockets.asyncio.client import ClientConnection from .channels import ChannelReader, ChannelWriter @@ -54,7 +55,6 @@ StreamListInput, StreamSetInput, ) -from .telemetry_types import OtelConfig from .triggers import Trigger, TriggerConfig, TriggerHandler, TriggerTypeRef from .types import Channel, RemoteFunctionData, RemoteTriggerTypeData, is_channel_ref @@ -224,7 +224,7 @@ async def connect_async(self) -> None: from an async context. """ self._running = True - from .telemetry import attach_event_loop, init_otel + from iii_observability.telemetry import attach_event_loop, init_otel loop = asyncio.get_running_loop() otel_cfg: OtelConfig | None = None @@ -280,7 +280,7 @@ async def shutdown_async(self) -> None: self._set_connection_state("disconnected") - from .telemetry import shutdown_otel_async + from iii_observability.telemetry import shutdown_otel_async await shutdown_otel_async() @@ -499,7 +499,7 @@ async def _invoke_with_otel_context( tracer = trace.get_tracer("iii-python-sdk") import os - from .payload import redact_and_truncate, resolve_max_bytes_from_env + from iii_observability import redact_and_truncate, resolve_max_bytes_from_env trace_payloads = os.environ.get("III_DISABLE_TRACE_PAYLOADS", "").lower() not in ( "1", diff --git a/sdk/packages/python/iii/src/iii/iii_constants.py b/sdk/packages/python/iii/src/iii/iii_constants.py index 439a7922b9..e550890ade 100644 --- a/sdk/packages/python/iii/src/iii/iii_constants.py +++ b/sdk/packages/python/iii/src/iii/iii_constants.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import Any, Callable, Literal -from .telemetry_types import OtelConfig +from iii_observability import OtelConfig, ReconnectionConfig IIIConnectionState = Literal["disconnected", "connecting", "connected", "reconnecting", "failed"] @@ -13,25 +13,6 @@ MAX_QUEUE_SIZE = 1000 -@dataclass -class ReconnectionConfig: - """Configuration for WebSocket reconnection behavior. - - Attributes: - initial_delay_ms: Starting delay in milliseconds. Default ``1000``. - max_delay_ms: Maximum delay cap in milliseconds. Default ``30000``. - backoff_multiplier: Exponential backoff multiplier. Default ``2.0``. - jitter_factor: Random jitter factor (0--1). Default ``0.3``. - max_retries: Maximum retry attempts. ``-1`` for infinite. Default ``-1``. - """ - - initial_delay_ms: int = 1000 - max_delay_ms: int = 30000 - backoff_multiplier: float = 2.0 - jitter_factor: float = 0.3 - max_retries: int = -1 - - DEFAULT_RECONNECTION_CONFIG = ReconnectionConfig() diff --git a/sdk/packages/python/iii/tests/test_baggage_span_processor.py b/sdk/packages/python/iii/tests/test_baggage_span_processor.py index 7cc2f4385f..4cbea255f7 100644 --- a/sdk/packages/python/iii/tests/test_baggage_span_processor.py +++ b/sdk/packages/python/iii/tests/test_baggage_span_processor.py @@ -10,7 +10,7 @@ ) from opentelemetry.sdk.trace.sampling import ALWAYS_OFF -from iii.baggage_span_processor import ( +from iii_observability import ( DEFAULT_ALLOWLIST, BaggageSpanProcessor, ) diff --git a/sdk/packages/python/iii/tests/test_context_propagation.py b/sdk/packages/python/iii/tests/test_context_propagation.py index e4f2ffb890..8a8236094a 100644 --- a/sdk/packages/python/iii/tests/test_context_propagation.py +++ b/sdk/packages/python/iii/tests/test_context_propagation.py @@ -7,8 +7,7 @@ import iii.iii as iii_module from iii.iii import III from iii.iii_constants import InitOptions -from iii.telemetry import init_otel, shutdown_otel -from iii.telemetry_types import OtelConfig +from iii_observability import OtelConfig, init_otel, shutdown_otel @pytest.fixture(autouse=True) diff --git a/sdk/packages/python/iii/tests/test_hold_process.py b/sdk/packages/python/iii/tests/test_hold_process.py index 6245aa3015..ce95e0a466 100644 --- a/sdk/packages/python/iii/tests/test_hold_process.py +++ b/sdk/packages/python/iii/tests/test_hold_process.py @@ -5,8 +5,8 @@ def test_background_thread_is_not_daemon(monkeypatch) -> None: """The background event-loop thread must be non-daemon so it keeps the process alive.""" - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) async def fake_do_connect(self: III) -> None: return None @@ -23,8 +23,8 @@ async def fake_do_connect(self: III) -> None: def test_shutdown_stops_background_thread(monkeypatch) -> None: """After shutdown(), the background thread should stop within a reasonable timeout.""" - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) async def fake_do_connect(self: III) -> None: return None @@ -43,8 +43,8 @@ async def fake_do_connect(self: III) -> None: def test_shutdown_async_stops_background_thread(monkeypatch) -> None: """After shutdown_async(), the background thread should also stop.""" - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) async def fake_do_connect(self: III) -> None: return None diff --git a/sdk/packages/python/iii/tests/test_http_external_functions_integration.py b/sdk/packages/python/iii/tests/test_http_external_functions_integration.py index acab3b5694..ebd53dad27 100644 --- a/sdk/packages/python/iii/tests/test_http_external_functions_integration.py +++ b/sdk/packages/python/iii/tests/test_http_external_functions_integration.py @@ -157,8 +157,8 @@ async def fake_connect(_: str, **kwargs: object) -> FakeWs: return FakeWs() monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) return sent diff --git a/sdk/packages/python/iii/tests/test_iii_registration_dedup.py b/sdk/packages/python/iii/tests/test_iii_registration_dedup.py index fd9be6218d..36c1639806 100644 --- a/sdk/packages/python/iii/tests/test_iii_registration_dedup.py +++ b/sdk/packages/python/iii/tests/test_iii_registration_dedup.py @@ -17,7 +17,7 @@ def reset_otel(): # III.connect() calls init_otel() which sets global providers; # reset them so subsequent test files start with a clean slate. try: - from iii.telemetry import shutdown_otel + from iii_observability import shutdown_otel shutdown_otel() except Exception: @@ -73,8 +73,8 @@ async def fake_connect(_addr: str, **kwargs: object) -> FakeWebSocket: return ws monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) client = III("ws://fake") @@ -110,8 +110,8 @@ async def fake_connect(_addr: str, **kwargs: object) -> FakeWebSocket: return ws monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) client = III("ws://fake") @@ -153,8 +153,8 @@ async def fake_connect(_addr: str, **kwargs: object) -> FakeWebSocket: return ws monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) client = III("ws://fake") diff --git a/sdk/packages/python/iii/tests/test_init_api.py b/sdk/packages/python/iii/tests/test_init_api.py index d9db99b8a7..e1c16e27b5 100644 --- a/sdk/packages/python/iii/tests/test_init_api.py +++ b/sdk/packages/python/iii/tests/test_init_api.py @@ -11,8 +11,8 @@ def test_register_worker_returns_iii_instance(monkeypatch) -> None: async def fake_do_connect(self: III) -> None: return None - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(III, "_do_connect", fake_do_connect) client = register_worker("ws://fake") @@ -28,7 +28,7 @@ def test_register_worker_is_sync() -> None: def test_connect_consumes_otel_from_init_options(monkeypatch) -> None: - import iii.telemetry as telemetry + import iii_observability.telemetry as telemetry captured = {"config": None} diff --git a/sdk/packages/python/iii/tests/test_logger_function_ids.py b/sdk/packages/python/iii/tests/test_logger_function_ids.py index 0787de7915..a034cee719 100644 --- a/sdk/packages/python/iii/tests/test_logger_function_ids.py +++ b/sdk/packages/python/iii/tests/test_logger_function_ids.py @@ -1,6 +1,6 @@ import logging -from iii.logger import Logger +from iii_observability import Logger def test_logger_uses_service_name() -> None: diff --git a/sdk/packages/python/iii/tests/test_logger_otel.py b/sdk/packages/python/iii/tests/test_logger_otel.py index b5df17d7df..b3ae00e9d6 100644 --- a/sdk/packages/python/iii/tests/test_logger_otel.py +++ b/sdk/packages/python/iii/tests/test_logger_otel.py @@ -7,7 +7,7 @@ @pytest.fixture(autouse=True) def reset_otel(): - from iii.telemetry import shutdown_otel + from iii_observability import shutdown_otel yield shutdown_otel() @@ -49,9 +49,7 @@ def _setup_in_memory_log_provider(): def test_logger_emits_otel_record_when_initialized(): """Logger.info emits an OTel LogRecord with severity INFO when OTel is active.""" - from iii.logger import Logger - from iii.telemetry import init_otel - from iii.telemetry_types import OtelConfig + from iii_observability import Logger, OtelConfig, init_otel log_exporter = _setup_in_memory_log_provider() init_otel(OtelConfig(enabled=True, logs_enabled=False)) # skip EngineLogExporter @@ -68,11 +66,11 @@ def test_logger_emits_otel_record_when_initialized(): def test_logger_emits_warn_severity(): from opentelemetry._logs import SeverityNumber - from iii.logger import Logger + from iii_observability import Logger log_exporter = _setup_in_memory_log_provider() - with patch("iii.logger._is_initialized", return_value=True): + with patch("iii_observability.logger._is_initialized", return_value=True): logger = Logger() logger.warn("watch out") @@ -86,7 +84,7 @@ def test_logger_attaches_trace_context_from_active_span(): from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from iii.logger import Logger + from iii_observability import Logger log_exporter = _setup_in_memory_log_provider() @@ -94,7 +92,7 @@ def test_logger_attaches_trace_context_from_active_span(): trace.set_tracer_provider(tracer_provider) tracer = tracer_provider.get_tracer("test") - with patch("iii.logger._is_initialized", return_value=True): + with patch("iii_observability.logger._is_initialized", return_value=True): with tracer.start_as_current_span("test-span") as span: logger = Logger(service_name="fn1") logger.info("inside span") @@ -110,11 +108,11 @@ def test_logger_attaches_trace_context_from_active_span(): def test_logger_no_trace_context_outside_span(): """Logger emits zero trace_id/span_id when no active span.""" - from iii.logger import Logger + from iii_observability import Logger log_exporter = _setup_in_memory_log_provider() - with patch("iii.logger._is_initialized", return_value=True): + with patch("iii_observability.logger._is_initialized", return_value=True): logger = Logger(service_name="fn1") logger.info("outside span") diff --git a/sdk/packages/python/iii/tests/test_payload.py b/sdk/packages/python/iii/tests/test_payload.py index d5c87349ac..1b308ae7a4 100644 --- a/sdk/packages/python/iii/tests/test_payload.py +++ b/sdk/packages/python/iii/tests/test_payload.py @@ -1,6 +1,6 @@ import os -from iii.payload import ( +from iii_observability import ( REDACTED_PLACEHOLDER, redact, redact_and_truncate, diff --git a/sdk/packages/python/iii/tests/test_register_function_args.py b/sdk/packages/python/iii/tests/test_register_function_args.py index 91c2313217..9d08e49a18 100644 --- a/sdk/packages/python/iii/tests/test_register_function_args.py +++ b/sdk/packages/python/iii/tests/test_register_function_args.py @@ -44,8 +44,8 @@ async def fake_connect(_: str, **kwargs: object) -> FakeWebSocket: return ws monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) return ws diff --git a/sdk/packages/python/iii/tests/test_span_ops.py b/sdk/packages/python/iii/tests/test_span_ops.py index abd971d3be..4d88eb4819 100644 --- a/sdk/packages/python/iii/tests/test_span_ops.py +++ b/sdk/packages/python/iii/tests/test_span_ops.py @@ -6,7 +6,7 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace import StatusCode -from iii.span_ops import ( +from iii_observability import ( current_span_is_recording, record_span_event, set_current_span_attribute, diff --git a/sdk/packages/python/iii/tests/test_sync_api.py b/sdk/packages/python/iii/tests/test_sync_api.py index 5f0927cbcf..e8573eb973 100644 --- a/sdk/packages/python/iii/tests/test_sync_api.py +++ b/sdk/packages/python/iii/tests/test_sync_api.py @@ -46,8 +46,8 @@ async def fake_connect(_: str, **kwargs: object) -> FakeWebSocket: return ws monkeypatch.setattr(iii_module.websockets, "connect", fake_connect) - monkeypatch.setattr("iii.telemetry.init_otel", lambda **kwargs: None) - monkeypatch.setattr("iii.telemetry.attach_event_loop", lambda loop: None) + monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None) + monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None) monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None) return ws diff --git a/sdk/packages/python/iii/tests/test_telemetry.py b/sdk/packages/python/iii/tests/test_telemetry.py index f8227ba17e..8163e19ab0 100644 --- a/sdk/packages/python/iii/tests/test_telemetry.py +++ b/sdk/packages/python/iii/tests/test_telemetry.py @@ -4,8 +4,8 @@ import pytest -from iii.telemetry import _get_tracer, _is_initialized, init_otel, shutdown_otel, shutdown_otel_async -from iii.telemetry_types import OtelConfig +from iii_observability.telemetry import _get_tracer, _is_initialized, init_otel, shutdown_otel, shutdown_otel_async +from iii_observability import OtelConfig # URLLibInstrumentor patches OpenerDirector.open, not urlopen directly ORIGINAL_OPENER_OPEN = urllib.request.OpenerDirector.open @@ -87,8 +87,8 @@ def test_shutdown_without_init_is_safe(): def test_telemetry_apis_importable_from_submodules(): - from iii.telemetry import _get_tracer, _is_initialized, init_otel, shutdown_otel - from iii.telemetry_types import OtelConfig + from iii_observability.telemetry import _get_tracer, _is_initialized, init_otel, shutdown_otel + from iii_observability import OtelConfig assert callable(init_otel) assert callable(shutdown_otel) @@ -103,7 +103,7 @@ def test_init_configures_engine_span_exporter(): from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor - from iii.telemetry_exporters import EngineSpanExporter + from iii_observability.telemetry_exporters import EngineSpanExporter init_otel(OtelConfig(enabled=True)) provider = trace.get_tracer_provider() @@ -139,7 +139,7 @@ def test_shutdown_closes_connection(): import asyncio from unittest.mock import AsyncMock, patch - from iii.telemetry_exporters import SharedEngineConnection + from iii_observability.telemetry_exporters import SharedEngineConnection with ( patch.object(SharedEngineConnection, "start"), diff --git a/sdk/packages/python/iii/tests/test_telemetry_exporters.py b/sdk/packages/python/iii/tests/test_telemetry_exporters.py index a291567fab..92776e342e 100644 --- a/sdk/packages/python/iii/tests/test_telemetry_exporters.py +++ b/sdk/packages/python/iii/tests/test_telemetry_exporters.py @@ -6,7 +6,7 @@ import pytest -from iii.telemetry_exporters import EngineLogExporter, EngineSpanExporter, SharedEngineConnection +from iii_observability.telemetry_exporters import EngineLogExporter, EngineSpanExporter, SharedEngineConnection def _make_mock_connection(): @@ -184,7 +184,7 @@ def test_serialize_metrics_emits_empty_string_for_missing_scope_version(): the Python OTel SDK. The serializer must emit "" (not JSON null) so the Rust engine's String deserializer doesn't reject the payload. """ - from iii.telemetry_exporters import _serialize_metrics + from iii_observability.telemetry_exporters import _serialize_metrics from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Resource diff --git a/sdk/packages/python/iii/tests/test_telemetry_types.py b/sdk/packages/python/iii/tests/test_telemetry_types.py index 9ed931568f..01d4989cca 100644 --- a/sdk/packages/python/iii/tests/test_telemetry_types.py +++ b/sdk/packages/python/iii/tests/test_telemetry_types.py @@ -1,6 +1,6 @@ """Tests for OtelConfig dataclass.""" -from iii.telemetry_types import OtelConfig +from iii_observability import OtelConfig def test_otel_config_defaults(): diff --git a/sdk/packages/python/iii/tests/test_trace_helpers.py b/sdk/packages/python/iii/tests/test_trace_helpers.py index 769189d27f..afcfe39d2e 100644 --- a/sdk/packages/python/iii/tests/test_trace_helpers.py +++ b/sdk/packages/python/iii/tests/test_trace_helpers.py @@ -1,7 +1,7 @@ from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider -from iii.telemetry import current_span_id, current_trace_id +from iii_observability import current_span_id, current_trace_id def test_trace_helpers_follow_the_active_span() -> None: diff --git a/sdk/packages/python/iii/uv.lock b/sdk/packages/python/iii/uv.lock index 60070253d0..f2205bb524 100644 --- a/sdk/packages/python/iii/uv.lock +++ b/sdk/packages/python/iii/uv.lock @@ -194,6 +194,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -489,6 +498,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] +[[package]] +name = "h11" +version = "0.16.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" } +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" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -498,13 +544,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iii-observability" +version = "0.13.0.dev1" +source = { editable = "../observability" } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, + { name = "opentelemetry-api", specifier = ">=1.25" }, + { name = "opentelemetry-sdk", specifier = ">=1.25" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.30" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2" }, + { name = "websockets", specifier = ">=12.0" }, +] +provides-extras = ["dev"] + [[package]] name = "iii-sdk" -version = "0.12.0" +version = "0.13.0.dev1" source = { editable = "." } dependencies = [ + { name = "iii-observability" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "websockets" }, ] @@ -526,9 +598,9 @@ requires-dist = [ { name = "aiohttp", marker = "extra == 'dev'", specifier = ">=3.9" }, { name = "anyio", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "griffe", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "iii-observability", editable = "../observability" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, { name = "opentelemetry-api", specifier = ">=1.25" }, - { name = "opentelemetry-sdk", specifier = ">=1.25" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, diff --git a/sdk/packages/python/observability/README.md b/sdk/packages/python/observability/README.md new file mode 100644 index 0000000000..09b83c8bf7 --- /dev/null +++ b/sdk/packages/python/observability/README.md @@ -0,0 +1,5 @@ +# iii-observability + +OpenTelemetry + Logger primitives shared across the iii SDKs. + +See https://github.com/iii-hq/iii for the full project. diff --git a/sdk/packages/python/observability/pyproject.toml b/sdk/packages/python/observability/pyproject.toml new file mode 100644 index 0000000000..6a02158af4 --- /dev/null +++ b/sdk/packages/python/observability/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "iii-observability" +version = "0.13.0.dev1" +description = "OpenTelemetry and logging primitives shared across iii SDKs." +authors = [{ name = "III" }] +license = { text = "Apache-2.0" } +readme = "README.md" +requires-python = ">=3.10" +keywords = ["iii", "observability", "opentelemetry", "logger"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] +dependencies = [ + "opentelemetry-api>=1.25", + "opentelemetry-sdk>=1.25", + "httpx>=0.27", + "websockets>=12.0", +] + +[project.urls] +Homepage = "https://github.com/iii-hq/iii" +Repository = "https://github.com/iii-hq/iii" + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-cov>=6.0", + "pytest-httpx>=0.30", + "mypy>=1.8", + "ruff>=0.2", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/iii_observability"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] + +[tool.mypy] +python_version = "3.10" +strict = true + +[tool.pytest.ini_options] +addopts = "--cov=src/iii_observability --cov-branch --cov-report=term-missing" +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/sdk/packages/python/observability/src/iii_observability/__init__.py b/sdk/packages/python/observability/src/iii_observability/__init__.py new file mode 100644 index 0000000000..c299cc0911 --- /dev/null +++ b/sdk/packages/python/observability/src/iii_observability/__init__.py @@ -0,0 +1,58 @@ +"""iii-observability: shared OTel + Logger primitives.""" + +from .baggage_span_processor import DEFAULT_ALLOWLIST, BaggageSpanProcessor +from .http_instrumentation import execute_traced_request +from .logger import Logger +from .payload import ( + REDACTED_PLACEHOLDER, + redact, + redact_and_truncate, + resolve_max_bytes_from_env, +) +from .reconnection import ReconnectionConfig +from .span_ops import ( + current_span_is_recording, + record_span_event, + set_current_span_attribute, + set_current_span_error, +) +from .telemetry import ( + current_span_id, + current_trace_id, + extract_baggage, + extract_traceparent, + flush_otel, + init_otel, + inject_baggage, + inject_traceparent, + shutdown_otel, + with_span, +) +from .telemetry_types import OtelConfig + +__all__ = [ + "BaggageSpanProcessor", + "DEFAULT_ALLOWLIST", + "Logger", + "OtelConfig", + "REDACTED_PLACEHOLDER", + "ReconnectionConfig", + "current_span_id", + "current_span_is_recording", + "current_trace_id", + "execute_traced_request", + "extract_baggage", + "extract_traceparent", + "flush_otel", + "init_otel", + "inject_baggage", + "inject_traceparent", + "record_span_event", + "redact", + "redact_and_truncate", + "resolve_max_bytes_from_env", + "set_current_span_attribute", + "set_current_span_error", + "shutdown_otel", + "with_span", +] diff --git a/sdk/packages/python/iii/src/iii/baggage_span_processor.py b/sdk/packages/python/observability/src/iii_observability/baggage_span_processor.py similarity index 100% rename from sdk/packages/python/iii/src/iii/baggage_span_processor.py rename to sdk/packages/python/observability/src/iii_observability/baggage_span_processor.py diff --git a/sdk/packages/python/observability/src/iii_observability/http_instrumentation.py b/sdk/packages/python/observability/src/iii_observability/http_instrumentation.py new file mode 100644 index 0000000000..1bf8c917bb --- /dev/null +++ b/sdk/packages/python/observability/src/iii_observability/http_instrumentation.py @@ -0,0 +1,102 @@ +"""HTTP client auto-instrumentation for the iii Python SDK. + +Mirrors the Rust execute_traced_request shape: wraps an httpx Request in an +OTel CLIENT span with HTTP semantic-convention attributes, injects W3C +traceparent into outgoing headers, and records exceptions on network errors. +""" + +from __future__ import annotations + +import httpx +from opentelemetry import trace +from opentelemetry.propagate import inject +from opentelemetry.trace import SpanKind, Status, StatusCode + +_SAFE_REQUEST_HEADERS = ("content-type", "accept") +_SAFE_RESPONSE_HEADERS = ("content-type",) + + +def _span_name(method: str, path: str | None) -> str: + return f"{method} {path}" if path else method + + +async def execute_traced_request( + client: httpx.AsyncClient, + request: httpx.Request, +) -> httpx.Response: + """Execute an httpx Request inside an OTel CLIENT span. + + - Injects W3C traceparent into outgoing request headers. + - Records HTTP semantic-convention attributes on the span. + - Sets ERROR span status for responses with status >= 400. + - Records exceptions for network-level errors. + """ + url = request.url + method = request.method.upper() + path = url.path or None + query = url.query + query_str: str | None + if isinstance(query, bytes): + query_str = query.decode() if query else None + else: + query_str = query or None + + attributes: dict[str, str | int] = { + "http.request.method": method, + "url.full": str(url), + } + if url.host: + attributes["server.address"] = url.host + if url.scheme: + attributes["url.scheme"] = url.scheme + attributes["network.protocol.name"] = "http" + if path: + attributes["url.path"] = path + if url.port: + attributes["server.port"] = url.port + if query_str: + attributes["url.query"] = query_str + + tracer = trace.get_tracer("iii-python-sdk") + name = _span_name(method, path) + + with tracer.start_as_current_span(name, kind=SpanKind.CLIENT, attributes=attributes) as span: + carrier: dict[str, str] = {} + inject(carrier) + for k, v in carrier.items(): + request.headers[k] = v + + for h in _SAFE_REQUEST_HEADERS: + v = request.headers.get(h) + if v: + span.set_attribute(f"http.request.header.{h}", v) + if request.content: + span.set_attribute("http.request.body.size", len(request.content)) + + try: + response = await client.send(request) + except httpx.HTTPError as err: + span.record_exception(err) + span.set_status(Status(StatusCode.ERROR, str(err))) + span.set_attribute("error.type", type(err).__name__) + raise + + span.set_attribute("http.response.status_code", response.status_code) + cl = response.headers.get("content-length") + if cl: + try: + span.set_attribute("http.response.body.size", int(cl)) + except ValueError: + pass + for h in _SAFE_RESPONSE_HEADERS: + v = response.headers.get(h) + if v: + span.set_attribute(f"http.response.header.{h}", v) + + if response.status_code >= 400: + span.set_status(Status(StatusCode.ERROR, str(response.status_code))) + span.set_attribute("error.type", str(response.status_code)) + else: + span.set_status(Status(StatusCode.OK)) + + return response diff --git a/sdk/packages/python/iii/src/iii/logger.py b/sdk/packages/python/observability/src/iii_observability/logger.py similarity index 100% rename from sdk/packages/python/iii/src/iii/logger.py rename to sdk/packages/python/observability/src/iii_observability/logger.py diff --git a/sdk/packages/python/iii/src/iii/payload.py b/sdk/packages/python/observability/src/iii_observability/payload.py similarity index 100% rename from sdk/packages/python/iii/src/iii/payload.py rename to sdk/packages/python/observability/src/iii_observability/payload.py diff --git a/sdk/packages/python/observability/src/iii_observability/py.typed b/sdk/packages/python/observability/src/iii_observability/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/packages/python/observability/src/iii_observability/reconnection.py b/sdk/packages/python/observability/src/iii_observability/reconnection.py new file mode 100644 index 0000000000..044fc91319 --- /dev/null +++ b/sdk/packages/python/observability/src/iii_observability/reconnection.py @@ -0,0 +1,24 @@ +"""WebSocket reconnection configuration for iii observability connections.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class ReconnectionConfig: + """Configuration for WebSocket reconnection behavior. + + Attributes: + initial_delay_ms: Starting delay in milliseconds. Default ``1000``. + max_delay_ms: Maximum delay cap in milliseconds. Default ``30000``. + backoff_multiplier: Exponential backoff multiplier. Default ``2.0``. + jitter_factor: Random jitter factor (0--1). Default ``0.3``. + max_retries: Maximum retry attempts. ``-1`` for infinite. Default ``-1``. + """ + + initial_delay_ms: int = 1000 + max_delay_ms: int = 30000 + backoff_multiplier: float = 2.0 + jitter_factor: float = 0.3 + max_retries: int = -1 diff --git a/sdk/packages/python/iii/src/iii/span_ops.py b/sdk/packages/python/observability/src/iii_observability/span_ops.py similarity index 100% rename from sdk/packages/python/iii/src/iii/span_ops.py rename to sdk/packages/python/observability/src/iii_observability/span_ops.py diff --git a/sdk/packages/python/iii/src/iii/telemetry.py b/sdk/packages/python/observability/src/iii_observability/telemetry.py similarity index 95% rename from sdk/packages/python/iii/src/iii/telemetry.py rename to sdk/packages/python/observability/src/iii_observability/telemetry.py index 33eeed007a..5f67eb5206 100644 --- a/sdk/packages/python/iii/src/iii/telemetry.py +++ b/sdk/packages/python/observability/src/iii_observability/telemetry.py @@ -13,6 +13,8 @@ import uuid from typing import Any, cast +from opentelemetry import trace + from .telemetry_types import OtelConfig _tracer: Any = None @@ -327,6 +329,25 @@ def _patched_open(self: Any, fullurl: Any, data: Any = None, timeout: Any = sock _fetch_patched = True +async def flush_otel() -> None: + """Force-flush all OTel providers without tearing them down. + + Counterpart to :func:`shutdown_otel`. Use before short-lived process exits + where you want pending spans/metrics/logs delivered but plan to keep using + OTel afterwards. + """ + tracer_provider = trace.get_tracer_provider() + flushers: list[asyncio.Future[Any] | Any] = [] + if hasattr(tracer_provider, "force_flush"): + flushers.append(asyncio.to_thread(tracer_provider.force_flush)) + if _meter_provider is not None and hasattr(_meter_provider, "force_flush"): + flushers.append(asyncio.to_thread(_meter_provider.force_flush)) + if _log_provider is not None and hasattr(_log_provider, "force_flush"): + flushers.append(asyncio.to_thread(_log_provider.force_flush)) + if flushers: + await asyncio.gather(*flushers) + + def shutdown_otel() -> None: """Shut down OTel synchronously (best-effort; does not await WS flush).""" _reset_state() diff --git a/sdk/packages/python/iii/src/iii/telemetry_exporters.py b/sdk/packages/python/observability/src/iii_observability/telemetry_exporters.py similarity index 100% rename from sdk/packages/python/iii/src/iii/telemetry_exporters.py rename to sdk/packages/python/observability/src/iii_observability/telemetry_exporters.py diff --git a/sdk/packages/python/iii/src/iii/telemetry_types.py b/sdk/packages/python/observability/src/iii_observability/telemetry_types.py similarity index 100% rename from sdk/packages/python/iii/src/iii/telemetry_types.py rename to sdk/packages/python/observability/src/iii_observability/telemetry_types.py diff --git a/sdk/packages/python/observability/tests/__init__.py b/sdk/packages/python/observability/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/packages/python/observability/tests/test_http_instrumentation.py b/sdk/packages/python/observability/tests/test_http_instrumentation.py new file mode 100644 index 0000000000..1f69d641aa --- /dev/null +++ b/sdk/packages/python/observability/tests/test_http_instrumentation.py @@ -0,0 +1,22 @@ +import httpx +import pytest +from iii_observability import execute_traced_request + + +@pytest.mark.asyncio +async def test_execute_traced_request_returns_response(httpx_mock): + httpx_mock.add_response(url="https://example.com/api", text="ok", status_code=200) + async with httpx.AsyncClient() as client: + req = client.build_request("GET", "https://example.com/api") + res = await execute_traced_request(client, req) + assert res.status_code == 200 + assert res.text == "ok" + + +@pytest.mark.asyncio +async def test_execute_traced_request_records_error_status(httpx_mock): + httpx_mock.add_response(url="https://example.com/api", text="bad", status_code=500) + async with httpx.AsyncClient() as client: + req = client.build_request("GET", "https://example.com/api") + res = await execute_traced_request(client, req) + assert res.status_code == 500 diff --git a/sdk/packages/python/observability/tests/test_telemetry.py b/sdk/packages/python/observability/tests/test_telemetry.py new file mode 100644 index 0000000000..8b881a81e4 --- /dev/null +++ b/sdk/packages/python/observability/tests/test_telemetry.py @@ -0,0 +1,17 @@ +import pytest +from iii_observability import OtelConfig, ReconnectionConfig, current_span_id, current_trace_id + + +def test_otel_config_defaults_disabled(): + cfg = OtelConfig() + assert cfg.enabled is None # enabled=None means "read from env" + + +def test_reconnection_config_has_initial_delay(): + cfg = ReconnectionConfig() + assert cfg.initial_delay_ms > 0 + + +def test_current_ids_none_outside_span(): + assert current_span_id() is None + assert current_trace_id() is None diff --git a/sdk/packages/python/observability/uv.lock b/sdk/packages/python/observability/uv.lock new file mode 100644 index 0000000000..a879174699 --- /dev/null +++ b/sdk/packages/python/observability/uv.lock @@ -0,0 +1,752 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.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" } +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" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "iii-observability" +version = "0.13.0.dev1" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "websockets" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-httpx" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, + { name = "opentelemetry-api", specifier = ">=1.25" }, + { name = "opentelemetry-sdk", specifier = ">=1.25" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.30" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2" }, + { name = "websockets", specifier = ">=12.0" }, +] +provides-extras = ["dev"] + +[[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]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.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" } +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" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +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" } +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" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +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" } +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" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +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" } +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 = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-httpx" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/42/f53c58570e80d503ade9dd42ce57f2915d14bcbe25f6308138143950d1d6/pytest_httpx-0.36.2.tar.gz", hash = "sha256:05a56527484f7f4e8c856419ea379b8dc359c36801c4992fdb330f294c690356", size = 57683, upload-time = "2026-04-09T13:57:19.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/55/1fa65f8e4fceb19dd6daa867c162ad845d547f6058cd92b4b02384a44777/pytest_httpx-0.36.2-py3-none-any.whl", hash = "sha256:d42ebd5679442dc7bfb0c48e0767b6562e9bc4534d805127b0084171886a5e22", size = 20315, upload-time = "2026-04-09T13:57:18.587Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, + { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/sdk/packages/rust/iii-example/Cargo.toml b/sdk/packages/rust/iii-example/Cargo.toml index 6b6ded0c7b..a508929eba 100644 --- a/sdk/packages/rust/iii-example/Cargo.toml +++ b/sdk/packages/rust/iii-example/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] iii-sdk = { path = "../iii" } +iii-observability = { workspace = true } async-trait = "0.1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } schemars = "0.8" diff --git a/sdk/packages/rust/iii-example/src/http_example.rs b/sdk/packages/rust/iii-example/src/http_example.rs index ec6be5ebc1..0fc3d78b90 100644 --- a/sdk/packages/rust/iii-example/src/http_example.rs +++ b/sdk/packages/rust/iii-example/src/http_example.rs @@ -1,5 +1,6 @@ +use iii_observability::{Logger, execute_traced_request}; use iii_sdk::builtin_triggers::{HttpMethod, HttpTriggerConfig}; -use iii_sdk::{ApiRequest, ApiResponse, III, IIIError, IIITrigger, Logger, execute_traced_request}; +use iii_sdk::{ApiRequest, ApiResponse, III, IIIError, IIITrigger}; use serde_json::json; pub fn setup(iii: &III) { diff --git a/sdk/packages/rust/iii-example/src/logger_example.rs b/sdk/packages/rust/iii-example/src/logger_example.rs index ace5fb9b7f..c586082f21 100644 --- a/sdk/packages/rust/iii-example/src/logger_example.rs +++ b/sdk/packages/rust/iii-example/src/logger_example.rs @@ -1,4 +1,5 @@ -use iii_sdk::{III, IIIError, Logger, RegisterFunction}; +use iii_observability::Logger; +use iii_sdk::{III, IIIError, RegisterFunction}; use serde_json::{Value, json}; pub fn setup(iii: &III) { diff --git a/sdk/packages/rust/iii-example/src/main.rs b/sdk/packages/rust/iii-example/src/main.rs index 1f088b6b88..f5f7737cc9 100644 --- a/sdk/packages/rust/iii-example/src/main.rs +++ b/sdk/packages/rust/iii-example/src/main.rs @@ -1,8 +1,8 @@ use std::{thread::sleep, time::Duration}; +use iii_observability::OtelConfig; use iii_sdk::{ - InitOptions, OtelConfig, RegisterFunction, TriggerRequest, UpdateBuilder, UpdateOp, - register_worker, + InitOptions, RegisterFunction, TriggerRequest, UpdateBuilder, UpdateOp, register_worker, }; use serde_json::json; diff --git a/sdk/packages/rust/iii/Cargo.toml b/sdk/packages/rust/iii/Cargo.toml index 34f95ebaa5..32b3a0d804 100644 --- a/sdk/packages/rust/iii/Cargo.toml +++ b/sdk/packages/rust/iii/Cargo.toml @@ -28,16 +28,10 @@ tokio-tungstenite = { version = "0.28", features = ["rustls-tls-native-roots"] } tracing = "0.1" uuid = { version = "1", features = ["v4", "serde"] } -# OpenTelemetry -opentelemetry = { version = "0.31" } -opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "logs", "metrics", "trace"] } -opentelemetry-proto = { version = "0.31", features = ["gen-tonic-messages", "trace", "metrics", "logs", "with-serde"] } -prost = { version = "0.14" } -sysinfo = { version = "0.38" } +iii-observability = { workspace = true } -# HTTP client tracing +# HTTP client (non-traced; traced variant lives in iii-observability) reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -opentelemetry-http = { version = "0.31", features = ["reqwest"] } [dev-dependencies] ctor = "0.6.3" diff --git a/sdk/packages/rust/iii/README.md b/sdk/packages/rust/iii/README.md index 629283af6f..703f547186 100644 --- a/sdk/packages/rust/iii/README.md +++ b/sdk/packages/rust/iii/README.md @@ -164,7 +164,7 @@ async fn main() -> Result<(), Box> { ### Logger ```rust -use iii_sdk::Logger; +use iii_observability::Logger; let logger = Logger::new(Some("my-function".to_string())); logger.info("Processing started", None); diff --git a/sdk/packages/rust/iii/src/iii.rs b/sdk/packages/rust/iii/src/iii.rs index bf786de58e..66a48cdefb 100644 --- a/sdk/packages/rust/iii/src/iii.rs +++ b/sdk/packages/rust/iii/src/iii.rs @@ -43,8 +43,8 @@ use crate::{ types::{Channel, RemoteFunctionData, RemoteFunctionHandler, RemoteTriggerTypeData}, }; -use crate::telemetry; -use crate::telemetry::types::OtelConfig; +use iii_observability as telemetry; +use iii_observability::OtelConfig; const DEFAULT_TIMEOUT_MS: u64 = 30_000; @@ -315,7 +315,7 @@ type WsTx = futures_util::stream::SplitSink< /// Inject trace context headers for outbound messages. fn inject_trace_headers() -> (Option, Option) { - use crate::telemetry::context; + use iii_observability as context; (context::inject_traceparent(), context::inject_baggage()) } @@ -1608,11 +1608,11 @@ impl III { // We use FutureExt::with_context() instead of cx.attach() because // ContextGuard is !Send and can't be held across .await in tokio::spawn. let otel_cx = { - use crate::telemetry::context::extract_context; - use opentelemetry::trace::{SpanKind, TraceContextExt, Tracer}; + use iii_observability::extract_context; + use iii_observability::opentelemetry::trace::{SpanKind, TraceContextExt, Tracer}; let parent_cx = extract_context(traceparent.as_deref(), baggage.as_deref()); - let tracer = opentelemetry::global::tracer("iii-rust-sdk"); + let tracer = iii_observability::opentelemetry::global::tracer("iii-rust-sdk"); let span = tracer .span_builder(format!("call {}", function_id)) .with_kind(SpanKind::Server) @@ -1624,12 +1624,12 @@ impl III { .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false); - let payload_max_bytes = crate::telemetry::payload::resolve_max_bytes_from_env(); + let payload_max_bytes = iii_observability::resolve_max_bytes_from_env(); if trace_payloads { - use crate::telemetry::payload::redact_and_truncate; - use opentelemetry::KeyValue; - use opentelemetry::trace::TraceContextExt; + use iii_observability::opentelemetry::KeyValue; + use iii_observability::opentelemetry::trace::TraceContextExt; + use iii_observability::redact_and_truncate; let span = otel_cx.span(); if span.span_context().is_valid() { let (input_json, truncated) = redact_and_truncate(&data, payload_max_bytes); @@ -1644,14 +1644,14 @@ impl III { } let result = { - use opentelemetry::trace::FutureExt as OtelFutureExt; + use iii_observability::opentelemetry::trace::FutureExt as OtelFutureExt; handler(data).with_context(otel_cx.clone()).await }; if trace_payloads { - use crate::telemetry::payload::redact_and_truncate; - use opentelemetry::KeyValue; - use opentelemetry::trace::TraceContextExt; + use iii_observability::opentelemetry::KeyValue; + use iii_observability::opentelemetry::trace::TraceContextExt; + use iii_observability::redact_and_truncate; let span = otel_cx.span(); if span.span_context().is_valid() { let (output_json, truncated, ok) = match &result { @@ -1679,8 +1679,8 @@ impl III { // Record span status based on result let mut error_stacktrace: Option = None; { - use opentelemetry::KeyValue; - use opentelemetry::trace::{Status, TraceContextExt}; + use iii_observability::opentelemetry::KeyValue; + use iii_observability::opentelemetry::trace::{Status, TraceContextExt}; let span = otel_cx.span(); match &result { Ok(_) => span.set_status(Status::Ok), diff --git a/sdk/packages/rust/iii/src/lib.rs b/sdk/packages/rust/iii/src/lib.rs index ed3411d628..72c8faab8b 100644 --- a/sdk/packages/rust/iii/src/lib.rs +++ b/sdk/packages/rust/iii/src/lib.rs @@ -2,11 +2,9 @@ pub mod builtin_triggers; pub mod channels; pub mod error; pub mod iii; -pub mod logger; pub mod protocol; pub mod stream; pub mod structs; -pub mod telemetry; pub mod triggers; pub mod types; @@ -24,7 +22,6 @@ pub use iii::{ IntoFunctionRegistration, RegisterFunction, RegisterTriggerType, TriggerTypeRef, WorkerMetadata, iii_async_fn, iii_fn, }; -pub use logger::Logger; pub use protocol::{ EnqueueResult, ErrorBody, FunctionMessage, HttpAuthConfig, HttpInvocationConfig, HttpMethod, Message, RegisterFunctionMessage, RegisterTriggerInput, RegisterTriggerMessage, @@ -61,7 +58,7 @@ pub struct InitOptions { /// Custom HTTP headers sent during the WebSocket handshake. pub headers: Option>, /// OpenTelemetry configuration. - pub otel: Option, + pub otel: Option, } /// Create and return a connected SDK instance. The WebSocket connection is @@ -110,25 +107,3 @@ pub fn register_worker(address: &str, options: InitOptions) -> III { iii } - -// OpenTelemetry re-exports -pub use telemetry::{ - baggage_span_processor::{BaggageSpanProcessor, DEFAULT_ALLOWLIST}, - context::{ - CapturedContext, capture_otel_context, current_span_id, current_trace_id, extract_baggage, - extract_context, extract_traceparent, get_all_baggage, get_baggage_entry, inject_baggage, - inject_traceparent, remove_baggage_entry, run_with_baggage, set_baggage_entry, - }, - flush_otel, - http_instrumentation::execute_traced_request, - init_otel, - payload::{REDACTED_PLACEHOLDER, redact, redact_and_truncate, resolve_max_bytes_from_env}, - run_in_span, shutdown_otel, - span_ops::{ - current_span_is_recording, record_span_event, set_current_span_attribute, - set_current_span_error, - }, - types::OtelConfig, - types::ReconnectionConfig, - with_span, -}; diff --git a/sdk/packages/rust/iii/tests/init_api.rs b/sdk/packages/rust/iii/tests/init_api.rs index c14df3f5d0..c3a230ae51 100644 --- a/sdk/packages/rust/iii/tests/init_api.rs +++ b/sdk/packages/rust/iii/tests/init_api.rs @@ -12,7 +12,7 @@ async fn init_with_runtime_returns_sdk_instance() { #[tokio::test] async fn init_applies_otel_config_before_auto_connect() { - use iii_sdk::OtelConfig; + use iii_observability::OtelConfig; let client = register_worker( "ws://127.0.0.1:49134", diff --git a/sdk/packages/rust/iii/tests/payload_capture.rs b/sdk/packages/rust/iii/tests/payload_capture.rs index a23ed51cd9..5c290c8c3f 100644 --- a/sdk/packages/rust/iii/tests/payload_capture.rs +++ b/sdk/packages/rust/iii/tests/payload_capture.rs @@ -2,8 +2,8 @@ use std::sync::Mutex; -use iii_sdk::BaggageSpanProcessor; -use iii_sdk::telemetry::payload::redact_and_truncate; +use iii_observability::BaggageSpanProcessor; +use iii_observability::redact_and_truncate; use opentelemetry::trace::{Status, TraceContextExt, Tracer}; use opentelemetry::{Context, KeyValue}; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SimpleSpanProcessor}; diff --git a/sdk/packages/rust/iii/tests/span_ops_api.rs b/sdk/packages/rust/iii/tests/span_ops_api.rs index a8c09e35b1..746f80404e 100644 --- a/sdk/packages/rust/iii/tests/span_ops_api.rs +++ b/sdk/packages/rust/iii/tests/span_ops_api.rs @@ -2,7 +2,7 @@ use std::sync::Mutex; -use iii_sdk::{ +use iii_observability::{ BaggageSpanProcessor, current_span_is_recording, get_baggage_entry, record_span_event, run_with_baggage, set_current_span_attribute, set_current_span_error, }; diff --git a/sdk/packages/rust/observability/Cargo.toml b/sdk/packages/rust/observability/Cargo.toml new file mode 100644 index 0000000000..b0ed7eed22 --- /dev/null +++ b/sdk/packages/rust/observability/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "iii-observability" +version = "0.13.0-next.1" +edition = "2024" +rust-version = "1.85" +license = "Apache-2.0" +authors = ["Motia LLC"] +description = "OpenTelemetry and logging primitives shared across iii SDKs." +repository = "https://github.com/iii-hq/iii" +homepage = "https://github.com/iii-hq/iii" +keywords = ["iii", "observability", "opentelemetry", "logger"] +categories = ["api-bindings", "asynchronous", "development-tools"] + +[lib] +name = "iii_observability" +path = "src/lib.rs" + +[dependencies] +async-trait = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] } +tokio-tungstenite = { version = "0.28", features = ["rustls-tls-native-roots"] } +futures-util = "0.3" +tracing = "0.1" +uuid = { version = "1", features = ["v4", "serde"] } + +opentelemetry = { version = "0.31" } +opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "logs", "metrics", "trace"] } +opentelemetry-proto = { version = "0.31", features = ["gen-tonic-messages", "trace", "metrics", "logs", "with-serde"] } +prost = { version = "0.14" } +sysinfo = { version = "0.38" } + +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +opentelemetry-http = { version = "0.31", features = ["reqwest"] } + +[dev-dependencies] +ctor = "0.6.3" +opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "trace", "testing"] } +serial_test = "3" +tokio = { version = "1", features = ["test-util"] } diff --git a/sdk/packages/rust/observability/README.md b/sdk/packages/rust/observability/README.md new file mode 100644 index 0000000000..09b83c8bf7 --- /dev/null +++ b/sdk/packages/rust/observability/README.md @@ -0,0 +1,5 @@ +# iii-observability + +OpenTelemetry + Logger primitives shared across the iii SDKs. + +See https://github.com/iii-hq/iii for the full project. diff --git a/sdk/packages/rust/observability/src/lib.rs b/sdk/packages/rust/observability/src/lib.rs new file mode 100644 index 0000000000..5979abba01 --- /dev/null +++ b/sdk/packages/rust/observability/src/lib.rs @@ -0,0 +1,28 @@ +//! iii-observability: shared OTel + Logger primitives for the iii Rust SDK. +pub const VERSION: &str = "0.13.0-next.1"; + +/// Re-export the raw `opentelemetry` crate so dependents can use OTel API +/// types (traits, `KeyValue`, `global`, etc.) without a direct dep. +pub use opentelemetry; + +pub mod telemetry; + +pub use telemetry::baggage_span_processor::{BaggageSpanProcessor, DEFAULT_ALLOWLIST}; +pub use telemetry::context::{ + CapturedContext, capture_otel_context, current_span_id, current_trace_id, extract_baggage, + extract_context, extract_traceparent, get_all_baggage, get_baggage_entry, inject_baggage, + inject_traceparent, remove_baggage_entry, run_with_baggage, set_baggage_entry, +}; +pub use telemetry::http_instrumentation::execute_traced_request; +pub use telemetry::payload::{ + REDACTED_PLACEHOLDER, redact, redact_and_truncate, resolve_max_bytes_from_env, +}; +pub use telemetry::span_ops::{ + current_span_is_recording, record_span_event, set_current_span_attribute, + set_current_span_error, +}; +pub use telemetry::types::{OtelConfig, ReconnectionConfig}; +pub use telemetry::{flush_otel, init_otel, run_in_span, shutdown_otel, with_span}; + +pub mod logger; +pub use logger::Logger; diff --git a/sdk/packages/rust/iii/src/logger.rs b/sdk/packages/rust/observability/src/logger.rs similarity index 97% rename from sdk/packages/rust/iii/src/logger.rs rename to sdk/packages/rust/observability/src/logger.rs index d7959b7505..97acbdb17a 100644 --- a/sdk/packages/rust/iii/src/logger.rs +++ b/sdk/packages/rust/observability/src/logger.rs @@ -43,7 +43,7 @@ fn json_value_to_anyvalue(v: &Value) -> AnyValue { /// # Examples /// /// ```rust -/// use iii_sdk::Logger; +/// use iii_observability::Logger; /// use serde_json::json; /// /// let logger = Logger::new(); @@ -114,7 +114,7 @@ impl Logger { /// # Examples /// /// ```rust - /// # use iii_sdk::Logger; + /// # use iii_observability::Logger; /// # use serde_json::json; /// # let logger = Logger::new(); /// logger.info("Order processed", Some(json!({ "order_id": "ord_123", "status": "completed" }))); @@ -138,7 +138,7 @@ impl Logger { /// # Examples /// /// ```rust - /// # use iii_sdk::Logger; + /// # use iii_observability::Logger; /// # use serde_json::json; /// # let logger = Logger::new(); /// logger.warn("Retry attempt", Some(json!({ "attempt": 3, "max_retries": 5, "endpoint": "/api/charge" }))); @@ -162,7 +162,7 @@ impl Logger { /// # Examples /// /// ```rust - /// # use iii_sdk::Logger; + /// # use iii_observability::Logger; /// # use serde_json::json; /// # let logger = Logger::new(); /// logger.error("Payment failed", Some(json!({ "order_id": "ord_123", "gateway": "stripe", "error_code": "card_declined" }))); @@ -186,7 +186,7 @@ impl Logger { /// # Examples /// /// ```rust - /// # use iii_sdk::Logger; + /// # use iii_observability::Logger; /// # use serde_json::json; /// # let logger = Logger::new(); /// logger.debug("Cache lookup", Some(json!({ "key": "user:42", "hit": false }))); diff --git a/sdk/packages/rust/iii/src/telemetry/baggage_span_processor.rs b/sdk/packages/rust/observability/src/telemetry/baggage_span_processor.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/baggage_span_processor.rs rename to sdk/packages/rust/observability/src/telemetry/baggage_span_processor.rs diff --git a/sdk/packages/rust/iii/src/telemetry/connection.rs b/sdk/packages/rust/observability/src/telemetry/connection.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/connection.rs rename to sdk/packages/rust/observability/src/telemetry/connection.rs diff --git a/sdk/packages/rust/iii/src/telemetry/context.rs b/sdk/packages/rust/observability/src/telemetry/context.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/context.rs rename to sdk/packages/rust/observability/src/telemetry/context.rs diff --git a/sdk/packages/rust/iii/src/telemetry/http_instrumentation.rs b/sdk/packages/rust/observability/src/telemetry/http_instrumentation.rs similarity index 98% rename from sdk/packages/rust/iii/src/telemetry/http_instrumentation.rs rename to sdk/packages/rust/observability/src/telemetry/http_instrumentation.rs index fcfa73b614..1fa19841b4 100644 --- a/sdk/packages/rust/iii/src/telemetry/http_instrumentation.rs +++ b/sdk/packages/rust/observability/src/telemetry/http_instrumentation.rs @@ -8,7 +8,7 @@ //! //! # Example //! ```no_run -//! use iii_sdk::telemetry::http_instrumentation::execute_traced_request; +//! use iii_observability::telemetry::http_instrumentation::execute_traced_request; //! //! # async fn example() -> Result<(), reqwest::Error> { //! let client = reqwest::Client::new(); diff --git a/sdk/packages/rust/iii/src/telemetry/json_serializer.rs b/sdk/packages/rust/observability/src/telemetry/json_serializer.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/json_serializer.rs rename to sdk/packages/rust/observability/src/telemetry/json_serializer.rs diff --git a/sdk/packages/rust/iii/src/telemetry/log_exporter.rs b/sdk/packages/rust/observability/src/telemetry/log_exporter.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/log_exporter.rs rename to sdk/packages/rust/observability/src/telemetry/log_exporter.rs diff --git a/sdk/packages/rust/iii/src/telemetry/metrics_exporter.rs b/sdk/packages/rust/observability/src/telemetry/metrics_exporter.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/metrics_exporter.rs rename to sdk/packages/rust/observability/src/telemetry/metrics_exporter.rs diff --git a/sdk/packages/rust/iii/src/telemetry/mod.rs b/sdk/packages/rust/observability/src/telemetry/mod.rs similarity index 99% rename from sdk/packages/rust/iii/src/telemetry/mod.rs rename to sdk/packages/rust/observability/src/telemetry/mod.rs index e4e5106338..bbfae2f4a9 100644 --- a/sdk/packages/rust/iii/src/telemetry/mod.rs +++ b/sdk/packages/rust/observability/src/telemetry/mod.rs @@ -1,3 +1,4 @@ +//! Telemetry module: OTel init, context propagation, span helpers. pub mod baggage_span_processor; pub mod connection; pub mod context; diff --git a/sdk/packages/rust/iii/src/telemetry/otel_worker_gauges.rs b/sdk/packages/rust/observability/src/telemetry/otel_worker_gauges.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/otel_worker_gauges.rs rename to sdk/packages/rust/observability/src/telemetry/otel_worker_gauges.rs diff --git a/sdk/packages/rust/iii/src/telemetry/payload.rs b/sdk/packages/rust/observability/src/telemetry/payload.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/payload.rs rename to sdk/packages/rust/observability/src/telemetry/payload.rs diff --git a/sdk/packages/rust/iii/src/telemetry/span_exporter.rs b/sdk/packages/rust/observability/src/telemetry/span_exporter.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/span_exporter.rs rename to sdk/packages/rust/observability/src/telemetry/span_exporter.rs diff --git a/sdk/packages/rust/iii/src/telemetry/span_ops.rs b/sdk/packages/rust/observability/src/telemetry/span_ops.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/span_ops.rs rename to sdk/packages/rust/observability/src/telemetry/span_ops.rs diff --git a/sdk/packages/rust/iii/src/telemetry/types.rs b/sdk/packages/rust/observability/src/telemetry/types.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/types.rs rename to sdk/packages/rust/observability/src/telemetry/types.rs diff --git a/sdk/packages/rust/iii/src/telemetry/worker_metrics.rs b/sdk/packages/rust/observability/src/telemetry/worker_metrics.rs similarity index 100% rename from sdk/packages/rust/iii/src/telemetry/worker_metrics.rs rename to sdk/packages/rust/observability/src/telemetry/worker_metrics.rs diff --git a/sdk/packages/rust/observability/tests/context_test.rs b/sdk/packages/rust/observability/tests/context_test.rs new file mode 100644 index 0000000000..710635933e --- /dev/null +++ b/sdk/packages/rust/observability/tests/context_test.rs @@ -0,0 +1,7 @@ +use iii_observability::{current_span_id, current_trace_id}; + +#[test] +fn outside_span_returns_none() { + assert_eq!(current_span_id(), None); + assert_eq!(current_trace_id(), None); +} diff --git a/sdk/packages/rust/observability/tests/init_test.rs b/sdk/packages/rust/observability/tests/init_test.rs new file mode 100644 index 0000000000..5f269a2716 --- /dev/null +++ b/sdk/packages/rust/observability/tests/init_test.rs @@ -0,0 +1,14 @@ +use iii_observability::OtelConfig; +use iii_observability::{flush_otel, init_otel, shutdown_otel}; + +#[tokio::test] +async fn init_otel_with_disabled_config_is_noop() { + let cfg = OtelConfig { + enabled: Some(false), + ..Default::default() + }; + let initialized = init_otel(cfg).await; + assert!(!initialized, "disabled config should return false"); + flush_otel().await; + shutdown_otel().await; +} diff --git a/sdk/packages/rust/observability/tests/logger_test.rs b/sdk/packages/rust/observability/tests/logger_test.rs new file mode 100644 index 0000000000..54e9cdfe6f --- /dev/null +++ b/sdk/packages/rust/observability/tests/logger_test.rs @@ -0,0 +1,6 @@ +use iii_observability::Logger; + +#[test] +fn logger_constructs() { + let _logger = Logger::new(); +} diff --git a/sdk/packages/rust/observability/tests/payload_test.rs b/sdk/packages/rust/observability/tests/payload_test.rs new file mode 100644 index 0000000000..7680575ba7 --- /dev/null +++ b/sdk/packages/rust/observability/tests/payload_test.rs @@ -0,0 +1,9 @@ +use iii_observability::{REDACTED_PLACEHOLDER, redact}; + +#[test] +fn redact_replaces_sensitive_keys() { + let input = serde_json::json!({ "password": "hunter2", "name": "ok" }); + let out = redact(&input); + assert_eq!(out["password"], REDACTED_PLACEHOLDER); + assert_eq!(out["name"], "ok"); +} diff --git a/sdk/packages/rust/observability/tests/types_test.rs b/sdk/packages/rust/observability/tests/types_test.rs new file mode 100644 index 0000000000..8c99b14030 --- /dev/null +++ b/sdk/packages/rust/observability/tests/types_test.rs @@ -0,0 +1,13 @@ +use iii_observability::{OtelConfig, ReconnectionConfig}; + +#[test] +fn otel_config_default_disabled() { + let cfg = OtelConfig::default(); + assert!(cfg.enabled.is_none()); // enabled is Option, None means not configured (disabled) +} + +#[test] +fn reconnection_config_default_has_initial_delay() { + let cfg = ReconnectionConfig::default(); + assert!(cfg.initial_delay_ms > 0); +} diff --git a/skills/iii-rust-sdk/SKILL.md b/skills/iii-rust-sdk/SKILL.md index da4689ebe2..0a91eab9c6 100644 --- a/skills/iii-rust-sdk/SKILL.md +++ b/skills/iii-rust-sdk/SKILL.md @@ -15,74 +15,41 @@ Full API reference: ## Install -Use Cargo: +Add to `Cargo.toml`: -```bash -cargo add iii-sdk +```toml +iii-sdk = "0.13" +# Optional: OpenTelemetry + Logger primitives live in a separate crate. +iii-observability = "0.13" ``` ## Key Types and Functions -| Export | Purpose | -| ------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `register_worker(url, InitOptions)` | Connect to the engine, returns `III` client | -| `InitOptions { metadata, headers, otel }` | Worker metadata, auth headers, OpenTelemetry config | -| `III::register_function(RegisterFunction::new(id, handler))` | Register a sync function using the builder API | -| `III::register_function(RegisterFunction::new_async(id, handler))` | Register an async function using the builder API | -| `III::register_function_with(message, handler_or_http_config)` | Register from a full message, including HTTP-invoked functions | -| `RegisterFunction` | Builder with `.description()` and `.metadata()` | -| `HttpInvocationConfig` / `HttpAuthConfig` | External HTTP endpoint invocation config | -| `IIITrigger` | Typed built-in trigger builders | -| `RegisterTriggerType` / `TriggerTypeRef` | Typed custom trigger type builders | -| `III::register_trigger(RegisterTriggerInput)` | Bind a trigger to a function | -| `III::trigger(TriggerRequest)` | Invoke a function | -| `TriggerAction::Void` | Fire-and-forget invocation | -| `TriggerAction::Enqueue { queue }` | Durable async invocation | -| `IIIError` | SDK, handler, timeout, and remote error type | -| `ChannelReader` / `ChannelWriter` / `extract_channel_refs()` | Binary/text channel APIs | -| `Logger` | Structured logs | -| `with_span`, `run_in_span`, `OtelConfig` | OpenTelemetry instrumentation | -| `execute_traced_request` | HTTP client with trace context propagation | +| Export | Purpose | +| -------------------------------------------------- | -------------------------------------------------------------------------------- | +| `register_worker(url, InitOptions)` | Connect to the engine, returns `III` client | +| `III::register_function(id, RegisterFunction::new(handler))` | Register a sync function. Accepts typed handlers (schemas auto-extracted via `schemars`) and `Fn(Value) -> Result` closures. Handler error type must be `IIIError`. | +| `III::register_function(id, RegisterFunction::new_async(handler))` | Async equivalent of `new`. Same dual-shape support. | +| `III::register_function(id, RegisterFunction::http(http_config))` | Register an HTTP-invoked function (Lambda, Workers, etc.) — no local handler. | +| `RegisterFunction` | Builder with `.description()`, `.metadata()`, `.request_format()`, `.response_format()` | +| `III::register_trigger(type, function_id, config)` | Bind a trigger to a function | +| `III::trigger(TriggerRequest)` | Invoke a function | +| `TriggerAction::Void` | Fire-and-forget invocation | +| `TriggerAction::Enqueue { queue }` | Durable async invocation | +| `IIIError` | Error type for handler failures | +| `Streams` | Helper for atomic stream CRUD | +| `with_span`, `get_tracer`, `get_meter` | OpenTelemetry (requires `otel` feature) | +| `execute_traced_request` | HTTP client with trace context propagation | ## Key Notes +- Add `features = ["otel"]` to `Cargo.toml` for OpenTelemetry support - Use `RegisterFunction::new("id", handler)` for sync handlers, `RegisterFunction::new_async("id", handler)` for async -- Handler input/output types that derive `schemars::JsonSchema` get auto-generated request and response schemas -- Chain `.description("...")` and `.metadata(json!(...))` on `RegisterFunction` to document the function +- Handler input types that derive `schemars::JsonSchema` get auto-generated request schemas +- Chain `.description("...")` on `RegisterFunction` to document the function - Keep the tokio runtime alive (e.g., `tokio::time::sleep` loop) for event processing - `register_trigger` returns `Ok(())` on success; propagate errors with `?` -## HTTP-Invoked Functions - -Use `register_function_with(RegisterFunctionMessage, HttpInvocationConfig)` when a function ID should call an external HTTP endpoint instead of a local Rust handler. - -Auth modes: - -- `HttpAuthConfig::Hmac { secret_key }` -- `HttpAuthConfig::Bearer { token_key }` -- `HttpAuthConfig::ApiKey { header, value_key }` - -`secret_key`, `token_key`, and `value_key` name environment variables. Do not put raw credentials in config. - -## Typed Trigger Builders - -- `RegisterTriggerType::new(id, description, handler)` registers a custom trigger type. -- Chain `.trigger_request_format::()` for trigger config schema and `.call_request_format::()` for function payload schema. -- The returned `TriggerTypeRef` has typed `register_trigger`, `register_trigger_with_metadata`, `register_function`, and `register_function_async`. -- Use `IIITrigger` for typed built-in trigger builders when authoring Rust workers that bind HTTP, cron, queue, state, stream, or log triggers. - -## Channels - -- `let channel = iii.create_channel(None).await?` -- Pass `channel.reader_ref` or `channel.writer_ref` through a trigger payload. -- Use `ChannelReader::new(engine_ws_base, &reader_ref)` and `ChannelWriter::new(engine_ws_base, &writer_ref)` when reconstructing refs from payloads. -- Use `next_binary()` for incremental chunks, `read_all()` for full buffers, and `send_message()` for text messages. -- Use `extract_channel_refs(&serde_json::Value)` to find refs inside nested payloads. - -## Errors - -`IIIError::Remote { code, message, stacktrace }` carries engine error codes such as `FORBIDDEN`, `TIMEOUT`, `function_not_found`, `function_not_invokable`, `invocation_failed`, and `invocation_stopped`. `IIIError::Timeout`, `IIIError::NotConnected`, `IIIError::Handler`, `IIIError::Serde`, and `IIIError::WebSocket` describe SDK/local failures. - ## Pattern Boundaries - For usage patterns and working examples, see `iii-functions-and-triggers`