openbox-sdk-python 1.3.0__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/CHANGELOG.md +17 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/PKG-INFO +18 -2
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/README.md +16 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/__init__.py +1 -1
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/events.py +11 -4
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/http.py +134 -36
- openbox_sdk_python-1.4.0/openbox_core/instrumentation/http_stream.py +185 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/manager.py +3 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/pyproject.toml +3 -2
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_event_classify.py +1 -1
- openbox_sdk_python-1.4.0/tests/contracts/test_lifecycle_errors.py +47 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_http_preflight_block.py +141 -27
- openbox_sdk_python-1.4.0/tests/instrumentation/test_http_stream_capture.py +245 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_manager_and_otel_lifecycle.py +63 -4
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/test_import_safety.py +1 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/test_sdk_version.py +11 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/uv.lock +101 -50
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/instructions/openbox-sdk-python.instructions.md +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/workflows/ci.yml +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.github/workflows/publish.yml +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.gitignore +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/.python-version +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/LICENSE +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/adapters/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/adapters/base.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/approvals.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/bootstrap.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/client.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/config.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/fake_core.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/hook_preflight.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/conformance/instrumentation.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/context.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/context.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/otel_spans.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/contracts/results.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/errors.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/gate.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/events.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/preflight.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/hooks/wrappers.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_okta.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_transitions.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/identity_types.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/db.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/file.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/function.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/llm.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/shared.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/jwk_thumbprint.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/propagation.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/provider.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/setup.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/span_processor.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/otel/trace_context.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/py.typed +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/runtime.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/sdk_version.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/serialization.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/diagnostics.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/event_rules.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/registry.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/validation/span_normalization.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/__init__.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/core_span.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/wire/evaluate_payload.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/workload_identity.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/identity_fixtures.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_approval_poll.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_auth_failure_classification.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_bootstrap.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_fail_modes.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_keycloak_workload_identity.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_transition_preflight.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/client/test_v2_endpoint_selection.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_bootstrap_mode.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_identity_verification_method.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/config/test_resolution_order.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/conformance/test_required_cases.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/context/test_bind_reset.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/context/test_trace_key.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_approval_parsing.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_patch.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_result_parsing.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/gate/test_diagnostics.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/gate/test_strict_failures.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/conftest.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/instrumented_env.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_db_block.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_db_redis_mongo.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_file_function_block.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_hook_failclosed_and_sync_approval.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_hook_runtime.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/instrumentation/test_http_urllib.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/runtime/test_runtime_delegation.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/generate_golden_fixture_from_temporal_signer.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/golden_temporal_signed_request.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/MANIFEST.sha256 +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/approval.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/auth-validate.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/evaluate.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/handoff.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/keypair.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/README.md +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/alg-none.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/alg-symmetric.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/body-tampered.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-jku.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-jwk.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/embedded-x5u.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/empty-jti.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/excessive-lifetime.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/expired.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/future-iat.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/htu-with-host.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/htu-with-query.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/malformed-jwt-truncated.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/malformed-jwt.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/missing-assertion.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/missing-exp.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/normal-assertion-on-transition-proof-route.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/paired-with-different-api-key.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/signature-by-other-key.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/transition-proof-on-validate-route.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/transition-proof-signed-by-other-credential.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/undersized-rsa-key.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/unknown-kid.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/v1-path.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-audience.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-deployment-id.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-http-method.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-iss.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-path.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-signed-agent-id.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-signed-organization-id.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-sub.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-transition-challenge.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-transition-purpose.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/tamper/wrong-typ.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/identity_v2/transition-proof.json +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_golden_signing.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_okta_unit.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_v2_fixtures.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_identity_v2_signing.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/signing/test_jwk_thumbprint.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/go_spandata_compat/go.mod +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/go_spandata_compat/main.go +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/span_fixtures.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_backend_compat.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_core_span_projection.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_flat_hook_contract.py +0 -0
- {openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/wire/test_hex_ids.py +0 -0
|
@@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
- Capture streamed text response bodies for sync/async `httpx` and `httpx2`
|
|
12
|
+
requests without consuming the stream ahead of the caller. Emit completed
|
|
13
|
+
telemetry once on EOF, close, or read failure, with bounded body capture and
|
|
14
|
+
the original span/activity identity preserved.
|
|
15
|
+
|
|
16
|
+
## [1.4.0] - 2026-10-02
|
|
17
|
+
|
|
10
18
|
### Added
|
|
19
|
+
- HTTP span capture for `httpx2.Client` and `httpx2.AsyncClient` when `httpx2`
|
|
20
|
+
is installed, alongside `httpx`, using the existing HTTP toggle and OpenBox
|
|
21
|
+
started/completed payload contract.
|
|
11
22
|
- **Okta identity bootstrap** — an `okta_ai_agent` runtime now needs only
|
|
12
23
|
`OPENBOX_API_URL`, `OPENBOX_API_KEY`, and `OPENBOX_OKTA_AGENT_PRIVATE_KEY`. The SDK
|
|
13
24
|
fetches the remaining identity metadata (agent id, organization id, deployment id,
|
|
@@ -41,6 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
52
|
- No failure path downgrades to v1, to an unsigned request, or to a different identity
|
|
42
53
|
method. A Core that predates the endpoint (`404`) produces actionable upgrade guidance.
|
|
43
54
|
|
|
55
|
+
## [1.3.1] - 2026-09-17
|
|
56
|
+
|
|
57
|
+
### Security
|
|
58
|
+
- Require `cryptography>=50,<51` to address CVE-2026-69247. No OpenBox SDK API or
|
|
59
|
+
runtime behavior changes are included in this patch release.
|
|
60
|
+
|
|
44
61
|
## [1.3.0] - 2026-07-30
|
|
45
62
|
|
|
46
63
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openbox-sdk-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK
|
|
5
5
|
Author-email: OpenBox Team <tino@openbox.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,7 +15,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
15
15
|
Classifier: Topic :: System :: Monitoring
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Python: >=3.11
|
|
18
|
-
Requires-Dist: cryptography<50
|
|
18
|
+
Requires-Dist: cryptography<51,>=50
|
|
19
19
|
Requires-Dist: httpx<1,>=0.28.0
|
|
20
20
|
Requires-Dist: opentelemetry-api<1.40.0,>=1.38.0
|
|
21
21
|
Requires-Dist: opentelemetry-sdk<1.40.0,>=1.38.0
|
|
@@ -82,6 +82,22 @@ uv sync --extra http # + HTTP instrumentation targets
|
|
|
82
82
|
uv sync --extra db # + DB instrumentation targets
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
HTTP instrumentation also captures `httpx2.Client` and `httpx2.AsyncClient`
|
|
86
|
+
requests when `httpx2` is installed by the host application. It uses the same
|
|
87
|
+
HTTP toggle and started/completed span payloads as `httpx`, including body
|
|
88
|
+
capture and credential-header redaction.
|
|
89
|
+
|
|
90
|
+
For streamed `httpx`/`httpx2` responses, completed telemetry is emitted when the
|
|
91
|
+
caller finishes reading the decoded body or closes the response. Text chunks are
|
|
92
|
+
copied as they pass through; the SDK does not drain or buffer the entire stream.
|
|
93
|
+
SSE responses retain their raw event text in `response_body`. Capture respects
|
|
94
|
+
`privacy.max_body_size` (characters); when that limit is disabled, streaming
|
|
95
|
+
capture still uses a 65,536-character bound. Binary bodies are not captured.
|
|
96
|
+
A response closed before transport EOF records the consumed prefix and sets
|
|
97
|
+
`attributes["openbox.http.response_body.partial"]` to `true`. This includes clients
|
|
98
|
+
that stop reading after an SSE end marker and then close the HTTP response.
|
|
99
|
+
Stream read failures retain their original exception and report partial evidence.
|
|
100
|
+
|
|
85
101
|
## Okta AI Agent identity (`okta_ai_agent`)
|
|
86
102
|
|
|
87
103
|
An agent whose OpenBox identity is verified against an Okta AI Agent credential
|
|
@@ -43,6 +43,22 @@ uv sync --extra http # + HTTP instrumentation targets
|
|
|
43
43
|
uv sync --extra db # + DB instrumentation targets
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
HTTP instrumentation also captures `httpx2.Client` and `httpx2.AsyncClient`
|
|
47
|
+
requests when `httpx2` is installed by the host application. It uses the same
|
|
48
|
+
HTTP toggle and started/completed span payloads as `httpx`, including body
|
|
49
|
+
capture and credential-header redaction.
|
|
50
|
+
|
|
51
|
+
For streamed `httpx`/`httpx2` responses, completed telemetry is emitted when the
|
|
52
|
+
caller finishes reading the decoded body or closes the response. Text chunks are
|
|
53
|
+
copied as they pass through; the SDK does not drain or buffer the entire stream.
|
|
54
|
+
SSE responses retain their raw event text in `response_body`. Capture respects
|
|
55
|
+
`privacy.max_body_size` (characters); when that limit is disabled, streaming
|
|
56
|
+
capture still uses a 65,536-character bound. Binary bodies are not captured.
|
|
57
|
+
A response closed before transport EOF records the consumed prefix and sets
|
|
58
|
+
`attributes["openbox.http.response_body.partial"]` to `true`. This includes clients
|
|
59
|
+
that stop reading after an SSE end marker and then close the HTTP response.
|
|
60
|
+
Stream read failures retain their original exception and report partial evidence.
|
|
61
|
+
|
|
46
62
|
## Okta AI Agent identity (`okta_ai_agent`)
|
|
47
63
|
|
|
48
64
|
An agent whose OpenBox identity is verified against an Okta AI Agent credential
|
|
@@ -37,7 +37,7 @@ from .errors import (
|
|
|
37
37
|
# governance; eagerly it can deadlock package init as a circular import, and
|
|
38
38
|
# lazily it can recurse unboundedly when a per-request header builder resolves
|
|
39
39
|
# the version. Keep in sync with pyproject.toml on release.
|
|
40
|
-
__version__ = "1.
|
|
40
|
+
__version__ = "1.4.0"
|
|
41
41
|
|
|
42
42
|
__all__ = [
|
|
43
43
|
"__version__",
|
|
@@ -221,12 +221,19 @@ def workflow_completed(
|
|
|
221
221
|
)
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
def _error_payload(error: str | Mapping[str, Any]) -> dict[str, Any]:
|
|
225
|
+
"""Core expects ErrorInfo for lifecycle failures, never a bare string."""
|
|
226
|
+
if isinstance(error, str):
|
|
227
|
+
return {"type": "Exception", "message": error}
|
|
228
|
+
return dict(error)
|
|
229
|
+
|
|
230
|
+
|
|
224
231
|
def workflow_failed(
|
|
225
232
|
*,
|
|
226
233
|
workflow_id: str,
|
|
227
234
|
run_id: str,
|
|
228
235
|
workflow_type: str,
|
|
229
|
-
error: str | None = None,
|
|
236
|
+
error: str | Mapping[str, Any] | None = None,
|
|
230
237
|
task_queue: str | None = None,
|
|
231
238
|
multi_agent_session_id: str | None = None,
|
|
232
239
|
timestamp: str | None = None,
|
|
@@ -237,7 +244,7 @@ def workflow_failed(
|
|
|
237
244
|
workflow_id, run_id, workflow_type, task_queue, multi_agent_session_id, extra
|
|
238
245
|
)
|
|
239
246
|
if error is not None:
|
|
240
|
-
payload["error"] = error
|
|
247
|
+
payload["error"] = _error_payload(error)
|
|
241
248
|
return EventEnvelope(
|
|
242
249
|
event_type=EventType.WORKFLOW_FAILED, payload=payload, timestamp=timestamp
|
|
243
250
|
)
|
|
@@ -283,7 +290,7 @@ def activity_completed(
|
|
|
283
290
|
activity_type: str,
|
|
284
291
|
task_queue: str | None = None,
|
|
285
292
|
result: Any = None,
|
|
286
|
-
error: str | None = None,
|
|
293
|
+
error: str | Mapping[str, Any] | None = None,
|
|
287
294
|
attempt: int | None = None,
|
|
288
295
|
multi_agent_session_id: str | None = None,
|
|
289
296
|
timestamp: str | None = None,
|
|
@@ -301,7 +308,7 @@ def activity_completed(
|
|
|
301
308
|
if result is not None:
|
|
302
309
|
payload["result"] = result
|
|
303
310
|
if error is not None:
|
|
304
|
-
payload["error"] = error
|
|
311
|
+
payload["error"] = _error_payload(error)
|
|
305
312
|
if attempt is not None:
|
|
306
313
|
payload["attempt"] = attempt
|
|
307
314
|
return EventEnvelope(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""HTTP wrappers — requests + httpx (sync/async)
|
|
1
|
+
"""HTTP wrappers — requests + httpx/httpx2 (sync/async).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
the real request is sent; raising
|
|
5
|
-
|
|
3
|
+
Each wrapper creates or receives an OTel span and invokes the hook runtime
|
|
4
|
+
BEFORE the real request is sent; raising via the adapter prevents the request.
|
|
5
|
+
The wrapper itself never interprets verdicts.
|
|
6
6
|
|
|
7
7
|
Self-instrumentation guard: URLs under any ignored prefix (always including
|
|
8
8
|
the OpenBox ``api_url``) are skipped so evaluate calls never govern
|
|
@@ -13,11 +13,15 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import contextlib
|
|
15
15
|
import contextvars
|
|
16
|
+
import importlib
|
|
16
17
|
import logging
|
|
17
18
|
import time
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
21
|
+
from ..context import activity_scope
|
|
20
22
|
from ..contracts.otel_spans import HookType
|
|
23
|
+
from ..hooks.events import resolve_context
|
|
24
|
+
from .http_stream import capture_async_response_stream, capture_response_stream
|
|
21
25
|
from .shared import get_hook_runtime
|
|
22
26
|
|
|
23
27
|
logger = logging.getLogger(__name__)
|
|
@@ -33,6 +37,8 @@ __all__ = [
|
|
|
33
37
|
"uninstall_httpx",
|
|
34
38
|
"install_httpx_body_capture",
|
|
35
39
|
"uninstall_httpx_body_capture",
|
|
40
|
+
"install_httpx2_body_capture",
|
|
41
|
+
"uninstall_httpx2_body_capture",
|
|
36
42
|
"install_urllib3",
|
|
37
43
|
"uninstall_urllib3",
|
|
38
44
|
"install_urllib",
|
|
@@ -386,14 +392,17 @@ def uninstall_httpx() -> None:
|
|
|
386
392
|
logger.debug("httpx uninstrument skipped", exc_info=True)
|
|
387
393
|
|
|
388
394
|
|
|
389
|
-
# ── httpx body capture (Client.send / AsyncClient.send patch)
|
|
395
|
+
# ── httpx/httpx2 body capture (Client.send / AsyncClient.send patch) ──────────
|
|
390
396
|
#
|
|
391
397
|
# Separate from OTel instrumentation: OTel hooks receive streams that cannot be
|
|
392
398
|
# consumed. Patching send lets us read the request body (buffered before send)
|
|
393
|
-
# and the response body
|
|
399
|
+
# and the response body safely. Unread streams are observed as the caller reads
|
|
400
|
+
# them; completed telemetry waits for stream consumption or explicit close.
|
|
394
401
|
|
|
395
|
-
|
|
396
|
-
|
|
402
|
+
# Each library has distinct client classes. Keep its originals independent,
|
|
403
|
+
# including in the wrapper closures, so installing/restoring one cannot affect
|
|
404
|
+
# requests through the other.
|
|
405
|
+
_original_httpx_sends: dict[str, tuple[Any, Any, Any]] = {}
|
|
397
406
|
|
|
398
407
|
|
|
399
408
|
def _pop_httpx_span() -> Any:
|
|
@@ -486,31 +495,59 @@ def _httpx_completed_fields(
|
|
|
486
495
|
}
|
|
487
496
|
|
|
488
497
|
|
|
498
|
+
def _stream_completed_fields(
|
|
499
|
+
request: Any, response: Any, span: Any, start: float,
|
|
500
|
+
request_body: str | None, request_headers: dict | None,
|
|
501
|
+
body: str | None, error: str | None, partial: bool,
|
|
502
|
+
) -> dict:
|
|
503
|
+
with contextlib.suppress(Exception):
|
|
504
|
+
span.set_attribute("openbox.http.response_body.partial", partial)
|
|
505
|
+
fields = _httpx_completed_fields(
|
|
506
|
+
request, response, int((time.perf_counter() - start) * 1e9),
|
|
507
|
+
request_body, request_headers, body, sanitize_headers(response.headers),
|
|
508
|
+
)
|
|
509
|
+
if error is not None:
|
|
510
|
+
fields["error"] = error
|
|
511
|
+
return fields
|
|
512
|
+
|
|
513
|
+
|
|
489
514
|
def install_httpx_body_capture() -> bool:
|
|
490
|
-
"""
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
515
|
+
"""Capture both stages for httpx; install AFTER ``install_httpx``."""
|
|
516
|
+
return _install_httpx_body_capture("httpx")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def install_httpx2_body_capture() -> bool:
|
|
520
|
+
"""Capture both stages for httpx2 when installed, using the same wire shape.
|
|
521
|
+
|
|
522
|
+
The send wrapper creates its own OTel span, so this does not require a
|
|
523
|
+
newer OTel HTTPX instrumentor with native httpx2 support.
|
|
524
|
+
"""
|
|
525
|
+
return _install_httpx_body_capture("httpx2")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _install_httpx_body_capture(module_name: str) -> bool:
|
|
529
|
+
if module_name in _original_httpx_sends:
|
|
495
530
|
return True
|
|
496
531
|
try:
|
|
497
|
-
|
|
532
|
+
library = importlib.import_module(module_name)
|
|
498
533
|
except ImportError:
|
|
499
|
-
logger.info("
|
|
534
|
+
logger.info("%s not available for body capture — deferred", module_name)
|
|
500
535
|
return False
|
|
501
536
|
|
|
502
|
-
|
|
503
|
-
|
|
537
|
+
original_send = library.Client.send
|
|
538
|
+
original_async_send = library.AsyncClient.send
|
|
504
539
|
|
|
505
540
|
def _patched_send(self: Any, request: Any, *args: Any, **kwargs: Any) -> Any:
|
|
506
541
|
runtime = get_hook_runtime()
|
|
507
542
|
url = str(getattr(request, "url", "") or "")
|
|
508
543
|
if runtime is None or should_ignore_url(url):
|
|
509
|
-
return
|
|
544
|
+
return original_send(self, request, *args, **kwargs)
|
|
510
545
|
request_body, request_headers = _capture_httpx_request(request)
|
|
511
546
|
span = _start_httpx_span(request)
|
|
547
|
+
context = resolve_context(runtime._store, span)
|
|
512
548
|
patch_token = _httpx_patch_span_var.set(span)
|
|
513
549
|
start = time.perf_counter()
|
|
550
|
+
deferred = False
|
|
514
551
|
try:
|
|
515
552
|
runtime.preflight(
|
|
516
553
|
span,
|
|
@@ -523,7 +560,32 @@ def install_httpx_body_capture() -> bool:
|
|
|
523
560
|
"request_body": request_body,
|
|
524
561
|
},
|
|
525
562
|
)
|
|
526
|
-
response =
|
|
563
|
+
response = original_send(self, request, *args, **kwargs)
|
|
564
|
+
if not response.is_stream_consumed:
|
|
565
|
+
# An unbound request must not acquire another activity's
|
|
566
|
+
# context just because its body is consumed later.
|
|
567
|
+
if context is None or not context.activity_id or not context.activity_type:
|
|
568
|
+
return response
|
|
569
|
+
|
|
570
|
+
def complete(body: str | None, error: str | None, partial: bool) -> None:
|
|
571
|
+
try:
|
|
572
|
+
fields = _stream_completed_fields(
|
|
573
|
+
request, response, span, start, request_body, request_headers,
|
|
574
|
+
body, error, partial,
|
|
575
|
+
)
|
|
576
|
+
with activity_scope(context, store=runtime._store):
|
|
577
|
+
runtime.completed(span, hook_type=HookType.HTTP_REQUEST, fields=fields)
|
|
578
|
+
finally:
|
|
579
|
+
span.end()
|
|
580
|
+
|
|
581
|
+
capture_response_stream(
|
|
582
|
+
response,
|
|
583
|
+
max_chars=runtime._runtime.config.privacy.max_body_size,
|
|
584
|
+
capture_text=_is_text_content_type(response.headers.get("content-type")),
|
|
585
|
+
complete=complete,
|
|
586
|
+
)
|
|
587
|
+
deferred = True
|
|
588
|
+
return response
|
|
527
589
|
duration_ns = int((time.perf_counter() - start) * 1e9)
|
|
528
590
|
response_body, response_headers = _capture_httpx_response(response)
|
|
529
591
|
runtime.completed(
|
|
@@ -542,18 +604,21 @@ def install_httpx_body_capture() -> bool:
|
|
|
542
604
|
return response
|
|
543
605
|
finally:
|
|
544
606
|
_httpx_patch_span_var.reset(patch_token)
|
|
545
|
-
|
|
546
|
-
|
|
607
|
+
if not deferred:
|
|
608
|
+
with contextlib.suppress(Exception):
|
|
609
|
+
span.end()
|
|
547
610
|
|
|
548
611
|
async def _patched_async_send(self: Any, request: Any, *args: Any, **kwargs: Any) -> Any:
|
|
549
612
|
runtime = get_hook_runtime()
|
|
550
613
|
url = str(getattr(request, "url", "") or "")
|
|
551
614
|
if runtime is None or should_ignore_url(url):
|
|
552
|
-
return await
|
|
615
|
+
return await original_async_send(self, request, *args, **kwargs)
|
|
553
616
|
request_body, request_headers = _capture_httpx_request(request)
|
|
554
617
|
span = _start_httpx_span(request)
|
|
618
|
+
context = resolve_context(runtime._store, span)
|
|
555
619
|
patch_token = _httpx_patch_span_var.set(span)
|
|
556
620
|
start = time.perf_counter()
|
|
621
|
+
deferred = False
|
|
557
622
|
try:
|
|
558
623
|
await runtime.apreflight(
|
|
559
624
|
span,
|
|
@@ -566,7 +631,32 @@ def install_httpx_body_capture() -> bool:
|
|
|
566
631
|
"request_body": request_body,
|
|
567
632
|
},
|
|
568
633
|
)
|
|
569
|
-
response = await
|
|
634
|
+
response = await original_async_send(self, request, *args, **kwargs)
|
|
635
|
+
if not response.is_stream_consumed:
|
|
636
|
+
if context is None or not context.activity_id or not context.activity_type:
|
|
637
|
+
return response
|
|
638
|
+
|
|
639
|
+
async def complete(body: str | None, error: str | None, partial: bool) -> None:
|
|
640
|
+
try:
|
|
641
|
+
fields = _stream_completed_fields(
|
|
642
|
+
request, response, span, start, request_body, request_headers,
|
|
643
|
+
body, error, partial,
|
|
644
|
+
)
|
|
645
|
+
with activity_scope(context, store=runtime._store):
|
|
646
|
+
await runtime.acompleted(
|
|
647
|
+
span, hook_type=HookType.HTTP_REQUEST, fields=fields
|
|
648
|
+
)
|
|
649
|
+
finally:
|
|
650
|
+
span.end()
|
|
651
|
+
|
|
652
|
+
capture_async_response_stream(
|
|
653
|
+
response,
|
|
654
|
+
max_chars=runtime._runtime.config.privacy.max_body_size,
|
|
655
|
+
capture_text=_is_text_content_type(response.headers.get("content-type")),
|
|
656
|
+
complete=complete,
|
|
657
|
+
)
|
|
658
|
+
deferred = True
|
|
659
|
+
return response
|
|
570
660
|
duration_ns = int((time.perf_counter() - start) * 1e9)
|
|
571
661
|
response_body, response_headers = _capture_httpx_response(response)
|
|
572
662
|
await runtime.acompleted(
|
|
@@ -585,29 +675,37 @@ def install_httpx_body_capture() -> bool:
|
|
|
585
675
|
return response
|
|
586
676
|
finally:
|
|
587
677
|
_httpx_patch_span_var.reset(patch_token)
|
|
588
|
-
|
|
589
|
-
|
|
678
|
+
if not deferred:
|
|
679
|
+
with contextlib.suppress(Exception):
|
|
680
|
+
span.end()
|
|
590
681
|
|
|
591
|
-
|
|
592
|
-
|
|
682
|
+
library.Client.send = _patched_send
|
|
683
|
+
library.AsyncClient.send = _patched_async_send
|
|
684
|
+
_original_httpx_sends[module_name] = (library, original_send, original_async_send)
|
|
593
685
|
return True
|
|
594
686
|
|
|
595
687
|
|
|
596
688
|
def uninstall_httpx_body_capture() -> None:
|
|
597
689
|
"""Restore httpx send (idempotent). Call BEFORE ``uninstall_httpx`` so the
|
|
598
690
|
original chain is unwound in reverse install order."""
|
|
599
|
-
|
|
600
|
-
|
|
691
|
+
_uninstall_httpx_body_capture("httpx")
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def uninstall_httpx2_body_capture() -> None:
|
|
695
|
+
"""Restore httpx2 send independently of httpx (idempotent)."""
|
|
696
|
+
_uninstall_httpx_body_capture("httpx2")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _uninstall_httpx_body_capture(module_name: str) -> None:
|
|
700
|
+
originals = _original_httpx_sends.pop(module_name, None)
|
|
701
|
+
if originals is None:
|
|
601
702
|
return
|
|
703
|
+
library, original_send, original_async_send = originals
|
|
602
704
|
try:
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
httpx.Client.send = _original_httpx_send
|
|
606
|
-
httpx.AsyncClient.send = _original_httpx_async_send
|
|
705
|
+
library.Client.send = original_send
|
|
706
|
+
library.AsyncClient.send = original_async_send
|
|
607
707
|
except Exception:
|
|
608
|
-
logger.debug("
|
|
609
|
-
_original_httpx_send = None
|
|
610
|
-
_original_httpx_async_send = None
|
|
708
|
+
logger.debug("%s body-capture restore skipped", module_name, exc_info=True)
|
|
611
709
|
|
|
612
710
|
|
|
613
711
|
# ── urllib3 (OTel URLLib3Instrumentor hooks) ─────────────────────────────────
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Bounded response capture while HTTPX callers consume decoded stream bytes.
|
|
2
|
+
|
|
3
|
+
Wrap response iterators instead of buffering the transport: HTTPX keeps ownership
|
|
4
|
+
of decompression, backpressure, and connection cleanup. Its raw iterator closes
|
|
5
|
+
the response before the decoded iterator flushes, so defer that internal close's
|
|
6
|
+
completion until the outer iterator finishes. An explicit close between chunks
|
|
7
|
+
completes immediately with the captured prefix.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import codecs
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Capture:
|
|
21
|
+
def __init__(self, response: Any, max_chars: int, capture_text: bool):
|
|
22
|
+
# Streaming capture must remain bounded even when ordinary body
|
|
23
|
+
# truncation is disabled. Keep one extra character for the wire gate's
|
|
24
|
+
# existing truncation diagnostic when a positive limit is configured.
|
|
25
|
+
self.limit = max_chars if max_chars and max_chars > 0 else 65536
|
|
26
|
+
self.remaining = self.limit + (1 if max_chars and max_chars > 0 else 0)
|
|
27
|
+
self.parts: list[str] = []
|
|
28
|
+
self.in_next = False
|
|
29
|
+
self.eof = False
|
|
30
|
+
self.finished = False
|
|
31
|
+
self.error: str | None = None
|
|
32
|
+
self.decoder = None
|
|
33
|
+
if capture_text:
|
|
34
|
+
try:
|
|
35
|
+
decoder = codecs.getincrementaldecoder(response.encoding or "utf-8")
|
|
36
|
+
except LookupError:
|
|
37
|
+
decoder = codecs.getincrementaldecoder("utf-8")
|
|
38
|
+
self.decoder = decoder(errors="replace")
|
|
39
|
+
|
|
40
|
+
def _append(self, text: str) -> None:
|
|
41
|
+
part = text[: self.remaining]
|
|
42
|
+
if part:
|
|
43
|
+
self.parts.append(part)
|
|
44
|
+
self.remaining -= len(part)
|
|
45
|
+
|
|
46
|
+
def feed(self, chunk: bytes) -> None:
|
|
47
|
+
if self.decoder is None or self.finished:
|
|
48
|
+
return
|
|
49
|
+
# Decode in bounded slices, including when a transport yields a large
|
|
50
|
+
# chunk. Incremental decoding preserves characters split across chunks.
|
|
51
|
+
for offset in range(0, len(chunk), 4096):
|
|
52
|
+
if self.remaining <= 0:
|
|
53
|
+
break
|
|
54
|
+
self._append(self.decoder.decode(chunk[offset : offset + 4096]))
|
|
55
|
+
|
|
56
|
+
def failed(self, error: BaseException) -> None:
|
|
57
|
+
if not isinstance(error, GeneratorExit) and self.error is None:
|
|
58
|
+
self.error = f"{type(error).__name__}: {error}"
|
|
59
|
+
|
|
60
|
+
def body(self) -> str | None:
|
|
61
|
+
if self.decoder is None:
|
|
62
|
+
return None
|
|
63
|
+
if self.remaining > 0:
|
|
64
|
+
self._append(self.decoder.decode(b"", final=True))
|
|
65
|
+
return "".join(self.parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def capture_response_stream(
|
|
69
|
+
response: Any,
|
|
70
|
+
*,
|
|
71
|
+
max_chars: int,
|
|
72
|
+
capture_text: bool,
|
|
73
|
+
complete: Callable[[str | None, str | None, bool], None],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Capture sync decoded bytes and complete once at EOF, close, or error."""
|
|
76
|
+
capture = _Capture(response, max_chars, capture_text)
|
|
77
|
+
original_iter = response.iter_bytes
|
|
78
|
+
original_close = response.close
|
|
79
|
+
|
|
80
|
+
def finish() -> None:
|
|
81
|
+
if capture.finished or capture.in_next:
|
|
82
|
+
return
|
|
83
|
+
capture.finished = True
|
|
84
|
+
try:
|
|
85
|
+
complete(capture.body(), capture.error, not capture.eof)
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.warning("stream completed-hook telemetry failed", exc_info=True)
|
|
88
|
+
|
|
89
|
+
def iter_bytes(*args: Any, **kwargs: Any):
|
|
90
|
+
iterator = iter(original_iter(*args, **kwargs))
|
|
91
|
+
try:
|
|
92
|
+
while True:
|
|
93
|
+
capture.in_next = True
|
|
94
|
+
try:
|
|
95
|
+
chunk = next(iterator)
|
|
96
|
+
except StopIteration:
|
|
97
|
+
capture.eof = True
|
|
98
|
+
break
|
|
99
|
+
finally:
|
|
100
|
+
capture.in_next = False
|
|
101
|
+
capture.feed(chunk)
|
|
102
|
+
yield chunk
|
|
103
|
+
except BaseException as exc:
|
|
104
|
+
capture.failed(exc)
|
|
105
|
+
raise
|
|
106
|
+
finally:
|
|
107
|
+
try:
|
|
108
|
+
capture.in_next = True
|
|
109
|
+
if hasattr(iterator, "close"):
|
|
110
|
+
iterator.close()
|
|
111
|
+
finally:
|
|
112
|
+
capture.in_next = False
|
|
113
|
+
finish()
|
|
114
|
+
|
|
115
|
+
def close() -> None:
|
|
116
|
+
try:
|
|
117
|
+
original_close()
|
|
118
|
+
except BaseException as exc:
|
|
119
|
+
capture.failed(exc)
|
|
120
|
+
raise
|
|
121
|
+
finally:
|
|
122
|
+
finish()
|
|
123
|
+
|
|
124
|
+
response.iter_bytes = iter_bytes
|
|
125
|
+
response.close = close
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def capture_async_response_stream(
|
|
129
|
+
response: Any,
|
|
130
|
+
*,
|
|
131
|
+
max_chars: int,
|
|
132
|
+
capture_text: bool,
|
|
133
|
+
complete: Callable[[str | None, str | None, bool], Awaitable[None]],
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Async counterpart; never prefetches chunks or schedules background work."""
|
|
136
|
+
capture = _Capture(response, max_chars, capture_text)
|
|
137
|
+
original_iter = response.aiter_bytes
|
|
138
|
+
original_close = response.aclose
|
|
139
|
+
|
|
140
|
+
async def finish() -> None:
|
|
141
|
+
if capture.finished or capture.in_next:
|
|
142
|
+
return
|
|
143
|
+
capture.finished = True
|
|
144
|
+
try:
|
|
145
|
+
await complete(capture.body(), capture.error, not capture.eof)
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.warning("stream completed-hook telemetry failed", exc_info=True)
|
|
148
|
+
|
|
149
|
+
async def aiter_bytes(*args: Any, **kwargs: Any):
|
|
150
|
+
iterator = original_iter(*args, **kwargs).__aiter__()
|
|
151
|
+
try:
|
|
152
|
+
while True:
|
|
153
|
+
capture.in_next = True
|
|
154
|
+
try:
|
|
155
|
+
chunk = await anext(iterator)
|
|
156
|
+
except StopAsyncIteration:
|
|
157
|
+
capture.eof = True
|
|
158
|
+
break
|
|
159
|
+
finally:
|
|
160
|
+
capture.in_next = False
|
|
161
|
+
capture.feed(chunk)
|
|
162
|
+
yield chunk
|
|
163
|
+
except BaseException as exc:
|
|
164
|
+
capture.failed(exc)
|
|
165
|
+
raise
|
|
166
|
+
finally:
|
|
167
|
+
try:
|
|
168
|
+
capture.in_next = True
|
|
169
|
+
if hasattr(iterator, "aclose"):
|
|
170
|
+
await iterator.aclose()
|
|
171
|
+
finally:
|
|
172
|
+
capture.in_next = False
|
|
173
|
+
await finish()
|
|
174
|
+
|
|
175
|
+
async def aclose() -> None:
|
|
176
|
+
try:
|
|
177
|
+
await original_close()
|
|
178
|
+
except BaseException as exc:
|
|
179
|
+
capture.failed(exc)
|
|
180
|
+
raise
|
|
181
|
+
finally:
|
|
182
|
+
await finish()
|
|
183
|
+
|
|
184
|
+
response.aiter_bytes = aiter_bytes
|
|
185
|
+
response.aclose = aclose
|
{openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/openbox_core/instrumentation/manager.py
RENAMED
|
@@ -77,6 +77,8 @@ class InstrumentationManager:
|
|
|
77
77
|
# the captured original send already carries the request hook.
|
|
78
78
|
if http_instrumentation.install_httpx_body_capture():
|
|
79
79
|
self._installed_targets.append("httpx_body_capture")
|
|
80
|
+
if http_instrumentation.install_httpx2_body_capture():
|
|
81
|
+
self._installed_targets.append("httpx2_body_capture")
|
|
80
82
|
if http_instrumentation.install_urllib3():
|
|
81
83
|
self._installed_targets.append("urllib3")
|
|
82
84
|
if http_instrumentation.install_urllib():
|
|
@@ -122,6 +124,7 @@ class InstrumentationManager:
|
|
|
122
124
|
# Reverse install order: unwind the send patch before OTel httpx.
|
|
123
125
|
http_instrumentation.uninstall_urllib()
|
|
124
126
|
http_instrumentation.uninstall_urllib3()
|
|
127
|
+
http_instrumentation.uninstall_httpx2_body_capture()
|
|
125
128
|
http_instrumentation.uninstall_httpx_body_capture()
|
|
126
129
|
http_instrumentation.uninstall_httpx()
|
|
127
130
|
http_instrumentation.uninstall_requests()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openbox-sdk-python"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "OpenBox base SDK - governance contracts, strict gate, identity/signing, evaluate client, context runtime, OTel span wire serialization, and generic instrumentation shared by every OpenBox framework SDK"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "OpenBox Team", email = "tino@openbox.ai" },
|
|
@@ -26,7 +26,7 @@ dependencies = [
|
|
|
26
26
|
"opentelemetry-api>=1.38.0,<1.40.0",
|
|
27
27
|
"opentelemetry-sdk>=1.38.0,<1.40.0",
|
|
28
28
|
"httpx>=0.28.0,<1",
|
|
29
|
-
"cryptography>=
|
|
29
|
+
"cryptography>=50,<51",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
@@ -53,6 +53,7 @@ db = [
|
|
|
53
53
|
|
|
54
54
|
[dependency-groups]
|
|
55
55
|
dev = [
|
|
56
|
+
"httpx2>=2.0.0,<3",
|
|
56
57
|
"pytest>=8.2",
|
|
57
58
|
"pytest-asyncio>=0.26.0,<0.27",
|
|
58
59
|
"ruff>=0.8.0",
|
{openbox_sdk_python-1.3.0 → openbox_sdk_python-1.4.0}/tests/contracts/test_event_classify.py
RENAMED
|
@@ -142,7 +142,7 @@ class TestFactoriesValidation:
|
|
|
142
142
|
|
|
143
143
|
def test_workflow_failed_carries_error(self):
|
|
144
144
|
body = workflow_failed(**WF, error="boom").to_payload_dict()
|
|
145
|
-
assert body["error"] == "boom"
|
|
145
|
+
assert body["error"] == {"type": "Exception", "message": "boom"}
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
class TestRfc3339:
|