021-python-clients 0.2.0__tar.gz → 0.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.
Files changed (65) hide show
  1. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/CHANGELOG.md +29 -0
  2. 021_python_clients-0.4.0/CONTRACT.md +319 -0
  3. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/CONTRIBUTING.md +17 -7
  4. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/PKG-INFO +141 -24
  5. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/README.md +138 -21
  6. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/pyproject.toml +5 -5
  7. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/__init__.py +5 -3
  8. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/_observe.py +90 -4
  9. 021_python_clients-0.4.0/src/clients_021/compute/__init__.py +1 -0
  10. 021_python_clients-0.4.0/src/clients_021/compute/kubernetes.py +314 -0
  11. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/email/resend.py +5 -1
  12. 021_python_clients-0.4.0/src/clients_021/http/__init__.py +11 -0
  13. 021_python_clients-0.4.0/src/clients_021/http/_observe.py +175 -0
  14. 021_python_clients-0.4.0/src/clients_021/http/httpx.py +280 -0
  15. 021_python_clients-0.4.0/src/clients_021/http/httpx2.py +76 -0
  16. 021_python_clients-0.4.0/src/clients_021/httpx2.py +5 -0
  17. 021_python_clients-0.4.0/src/clients_021/llm/__init__.py +1 -0
  18. 021_python_clients-0.4.0/src/clients_021/llm/bedrock.py +250 -0
  19. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/queue/qstash.py +1 -1
  20. 021_python_clients-0.4.0/src/clients_021/secrets/__init__.py +1 -0
  21. 021_python_clients-0.4.0/src/clients_021/secrets/secrets_manager.py +127 -0
  22. 021_python_clients-0.4.0/tests/test_bedrock.py +290 -0
  23. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_contract.py +1 -1
  24. 021_python_clients-0.4.0/tests/test_http_stream_lifecycle.py +270 -0
  25. 021_python_clients-0.4.0/tests/test_http_streaming.py +482 -0
  26. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_httpx2.py +8 -2
  27. 021_python_clients-0.4.0/tests/test_kubernetes.py +299 -0
  28. 021_python_clients-0.4.0/tests/test_secrets_manager.py +258 -0
  29. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/wheel_minimal.py +37 -2
  30. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/wheel_smoke.py +19 -3
  31. 021_python_clients-0.2.0/CONTRACT.md +0 -191
  32. 021_python_clients-0.2.0/src/clients_021/http.py +0 -131
  33. 021_python_clients-0.2.0/src/clients_021/httpx2.py +0 -93
  34. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/.gitignore +0 -0
  35. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/LICENSE +0 -0
  36. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/_aws.py +0 -0
  37. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/cache/__init__.py +0 -0
  38. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/cache/redis.py +0 -0
  39. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/crypto/__init__.py +0 -0
  40. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/crypto/kms.py +0 -0
  41. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/email/__init__.py +0 -0
  42. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/email/_message.py +0 -0
  43. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/email/ses.py +0 -0
  44. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/errors.py +0 -0
  45. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/kms.py +0 -0
  46. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/py.typed +0 -0
  47. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/queue/__init__.py +0 -0
  48. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/queue/sqs.py +0 -0
  49. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/redis.py +0 -0
  50. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/s3.py +0 -0
  51. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/storage/__init__.py +0 -0
  52. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/storage/s3.py +0 -0
  53. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/src/clients_021/storage/vercel_blob.py +0 -0
  54. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/__init__.py +0 -0
  55. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/conftest.py +0 -0
  56. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/qstash_local.py +0 -0
  57. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_email.py +0 -0
  58. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_http.py +0 -0
  59. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_kms.py +0 -0
  60. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_qstash.py +0 -0
  61. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_redis.py +0 -0
  62. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_s3.py +0 -0
  63. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_ses.py +0 -0
  64. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_sqs.py +0 -0
  65. {021_python_clients-0.2.0 → 021_python_clients-0.4.0}/tests/test_vercel_blob.py +0 -0
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes are recorded here by release-please from Conventional Commits.
4
4
 
5
+ ## [0.4.0](https://github.com/from021-io/python-clients/compare/v0.3.0...v0.4.0) (2026-10-22)
6
+
7
+
8
+ ### Features
9
+
10
+ * **compute:** add Kubernetes Jobs client over the HTTP client ([9b3c369](https://github.com/from021-io/python-clients/commit/9b3c36930f6a070d3fee9844e86c0d27f871346b))
11
+ * **llm:** add observed Bedrock Runtime client for SDKs ([5216293](https://github.com/from021-io/python-clients/commit/52162939416f58c610f4f4e217d911f5e5efdea9))
12
+ * **secrets:** add observed Secrets Manager client ([f14042f](https://github.com/from021-io/python-clients/commit/f14042f064ca61e8d267b338b033a12fd41690eb))
13
+
14
+ ## [0.3.0](https://github.com/from021-io/python-clients/compare/v0.2.0...v0.3.0) (2026-10-21)
15
+
16
+
17
+ ### ⚠ BREAKING CHANGES
18
+
19
+ * **http:** HTTP durations include body consumption and cleanup, counts emit at completion and all HTTP series gain http.response.body.complete. Attempt failure logs are enabled by default. See CONTRACT.md for dashboard and consumer migration notes.
20
+
21
+ ### Features
22
+
23
+ * **http:** observe complete response bodies and streams ([333cece](https://github.com/from021-io/python-clients/commit/333cece69c41a9ef16828f83e303be4183a5a187))
24
+
25
+
26
+ ### Refactors
27
+
28
+ * **http:** group clients and transports by capability ([1a7b41a](https://github.com/from021-io/python-clients/commit/1a7b41a5dba214fb109acccc9e3e08a9d123e3ee))
29
+
5
30
  ## [0.2.0](https://github.com/from021-io/python-clients/compare/v0.1.0...v0.2.0) (2026-10-21)
6
31
 
7
32
 
@@ -22,10 +47,14 @@ All notable changes are recorded here by release-please from Conventional Commit
22
47
 
23
48
  ### Added
24
49
 
50
+ - Add `HttpClient.stream` and a native httpx `ObservedTransport` for incremental bodies with explicit lifetime and no streaming retries.
51
+
25
52
  - Add an optional Vercel Blob client with observed CRUD, explicit public/private access, bounded operations and SDK-native metadata/errors.
26
53
 
27
54
  ### Changed
28
55
 
56
+ - **Breaking — HTTP telemetry:** HTTP/httpx2 spans and request durations now include body consumption and resource close. Counters are emitted at completion, gain `http.response.body.complete`, and failures produce sanitized attempt logs. QStash/Resend retain one logical failure log. Cancellation closes resources before propagating; closed HTTP clients cannot reopen.
57
+
29
58
  - Group Redis, S3 and KMS under cache, storage and crypto, retaining the 0.1.0 imports as compatibility aliases.
30
59
 
31
60
  ## 0.1.0 (2026-10-21)
@@ -0,0 +1,319 @@
1
+ # Contract
2
+
3
+ What dashboards, alerts and services may rely on. Changing a name, a label or the error
4
+ semantics below is a breaking change.
5
+
6
+ ## Imports and capability packages
7
+
8
+ Queue providers live in `clients_021.queue.sqs` (`SqsClient`, `ReceivedMessage`) and
9
+ `clients_021.queue.qstash` (`QStashClient`, `PublishedMessage`, `QStashResponseError`).
10
+ Use these imports in place of the initial draft's root-level `clients_021.sqs` and
11
+ `clients_021.qstash` paths. These capability paths shipped in 0.1.0.
12
+ Importing `clients_021.queue` or its QStash module does not require the `aws` extra.
13
+
14
+ The queue package groups providers by purpose. SQS owns polling, receipts and visibility;
15
+ QStash owns HTTP delivery. Their public methods and observability contracts remain distinct.
16
+ Shared email types and the interchangeable `EmailSender` protocol live in `clients_021.email`.
17
+
18
+ Redis lives in `clients_021.cache.redis`, S3 in `clients_021.storage.s3`, KMS in
19
+ `clients_021.crypto.kms`, Secrets Manager in `clients_021.secrets.secrets_manager`, Bedrock
20
+ in `clients_021.llm.bedrock` and Kubernetes Jobs in `clients_021.compute.kubernetes`, which
21
+ needs no extra. The 0.1.0 root modules (`redis`, `s3`, `kms`) continue to re-export
22
+ the same clients and result types (`ObjectInfo`, `DataKey`). Existing call signatures,
23
+ metric/span names and error semantics are unchanged. Capability package imports do not
24
+ load optional SDKs. HTTP implementations live in `clients_021.http.httpx` and
25
+ `clients_021.http.httpx2`. `clients_021.http` retains `HttpClient`, `HttpResponse`,
26
+ `HttpMethod`, `IDEMPOTENT_METHODS` and `RETRY_STATUS`; the root `clients_021` exports remain
27
+ unchanged. `clients_021.httpx2.ObservedTransport` re-exports the same class as the new path.
28
+ Importing `clients_021.http` does not require the `httpx2` extra.
29
+
30
+ ## Metrics
31
+
32
+ All on the service's `Observability` meter, so they carry its resource (service name,
33
+ version, environment). Durations use 021-python-observability's buckets.
34
+
35
+ | Instrument | Type / unit | Labels | Clients |
36
+ | --- | --- | --- | --- |
37
+ | `db.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Redis (shared with SQL from 021-python-observability, which has no `operation`) |
38
+ | `db.client.operation.duration` | Histogram / `s` | same | Redis |
39
+ | `messaging.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | SQS, QStash, email (Resend, SES) |
40
+ | `messaging.client.operation.duration` | Histogram / `s` | same | SQS, QStash, email (Resend, SES) |
41
+ | `storage.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | S3, Vercel Blob |
42
+ | `storage.client.operation.duration` | Histogram / `s` | same | S3, Vercel Blob |
43
+ | `crypto.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | KMS |
44
+ | `crypto.client.operation.duration` | Histogram / `s` | same | KMS |
45
+ | `secrets.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Secrets Manager |
46
+ | `secrets.client.operation.duration` | Histogram / `s` | same | Secrets Manager |
47
+ | `llm.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Bedrock |
48
+ | `llm.client.operation.duration` | Histogram / `s` | same | Bedrock |
49
+ | `llm.client.token.usage` | Counter / `{token}` | dependency, operation, token.type | Bedrock (Converse and ConverseStream) |
50
+ | `compute.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Kubernetes Jobs |
51
+ | `compute.client.operation.duration` | Histogram / `s` | same | Kubernetes Jobs |
52
+ | `http.client.request.count` | Counter / `{request}` | dependency, http.request.method, http.response.status_code, error.type, http.response.body.complete | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
53
+ | `http.client.request.duration` | Histogram / `s` | same | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
54
+
55
+ - `status`: `ok` or `error`. `error.type` only on errors.
56
+ - `operation`: a fixed set per client. Redis: `get_json`, `set_json`, `take_json`,
57
+ `delete`, `incr_window`, `ping`. SQS: `send`, `receive`, `delete`, `change_visibility`.
58
+ S3 and Vercel Blob: `put`, `get`, `head`, `delete`. QStash: `publish`.
59
+ Email (Resend, SES): `send`. KMS:
60
+ `encrypt`, `decrypt`, `generate_data_key`; `seal` and `unseal` record the KMS call they make
61
+ (`generate_data_key`, `decrypt`), not the local AES work. Secrets Manager: `get`.
62
+ Bedrock: `converse`, `converse_stream`, `invoke_model`, `invoke_model_with_response_stream`,
63
+ `count_tokens`. A streaming operation ends when its event stream is exhausted, fails or is
64
+ closed, not when the call returns. Kubernetes Jobs: `create`, `get`, `delete`; each also
65
+ records its HTTP request (`http.client.request.*`) under the same `dependency`. A missing
66
+ Job (`get`, `delete`) and an existing one (`create`) are `status=ok`.
67
+ - `token.type`: `input`, `output`, `cache_read`, `cache_write`, as reported by Bedrock in
68
+ the Converse response or the ConverseStream `metadata` event. InvokeModel bodies are
69
+ model-specific and are not read.
70
+ - Redis/SQS/S3/Vercel Blob/QStash/email/KMS/Secrets Manager/Bedrock/Kubernetes Jobs `error.type`: `timeout`, `connection_error`, `throttled`,
71
+ `status_<5xx>`, `cancelled`, or the exception class name for errors that are not outages
72
+ (`EmailRejected` for every email rejection; botocore's class, such as `ClientError` or
73
+ `NotFoundException`, for KMS).
74
+ - Resend records two levels: the `send` operation (`messaging.client.operation.*`) and,
75
+ inside it, its HTTP request (`http.client.request.*`), both under the same `dependency`.
76
+ - HTTP transport metrics use the bounded labels (`timeout`, `cancelled`,
77
+ `connection_error` or `internal_error`). These describe the transport exception, while
78
+ `DependencyUnavailable.reason` describes the failure returned to the caller.
79
+ - httpx2 uses `timeout`, `connection_error`, `cancelled`, or the exception class name.
80
+ HTTP status errors set the span's ERROR status and status-code label, with no `error.type`.
81
+ - A missing S3/Blob object or Redis key is `status=ok`.
82
+ - `dependency` is a static identifier, validated consistently with 021-python-observability.
83
+ User ids, tenant ids and request-derived values are forbidden even if they match its syntax.
84
+
85
+ ## Spans
86
+
87
+ CLIENT spans named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
88
+ metric labels plus `db.system.name=redis`, `messaging.system=aws_sqs`,
89
+ `messaging.system=upstash_qstash`, `messaging.system=resend`, `messaging.system=aws_ses`, or
90
+ `rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS` / `rpc.service=SecretsManager` /
91
+ `rpc.service=BedrockRuntime` (plus `gen_ai.system=aws.bedrock`), `rpc.system=vercel-blob`, or
92
+ `rpc.system=kubernetes` + `rpc.service=Job`. Kubernetes Jobs' `HTTP <dependency>` span is a
93
+ child of its `<dependency> <operation>` span.
94
+ Resend's `HTTP <dependency>` span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
95
+ status ≥ 400).
96
+ Cancellation propagates unchanged and records ERROR with `error.type=cancelled`.
97
+ HTTP transports measure from sending the request through response-body EOF and resource
98
+ close, or explicit early close. Uploads and incremental downloads are included, as are
99
+ consumer pauses between chunks and cleanup latency. Each attempt ends its span and emits
100
+ one count and one duration exactly once. An HTTP 200 followed by a body timeout is ERROR
101
+ with status code 200 and `error.type=timeout`; the status code describes the headers only.
102
+
103
+ `http.response.body.complete` is a boolean on HTTP spans and metric points: true after
104
+ response-body EOF, false when no response arrived or it was closed before EOF. It does not
105
+ imply success: an HTTP rejection or close failure can still set ERROR. An intentional
106
+ partial close without an error does not set ERROR. Cancellation sets `error.type=cancelled`,
107
+ including cancellation while reading, closing or waiting between chunks in a stream context.
108
+
109
+ The HTTP span is current only while sending, reading, closing and emitting its log. It is
110
+ not left attached while a caller processes a chunk; interleaved and cross-task consumers
111
+ keep their own context. All HTTP attempt spans retain the parent active at request start.
112
+
113
+ QStash adds one logical `publish` operation around the HTTP transport request. The
114
+ messaging duration includes reading and validating the acknowledgement; the nested HTTP
115
+ span includes reading and closing the response body. Both use the configured static dependency name.
116
+ Local input validation occurs before either operation is recorded.
117
+
118
+ ## Logging and coverage limits
119
+
120
+ Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a structured
121
+ `dependency.call_failed` warning when an observed remote operation raises an exception.
122
+ Fields are `dependency`, `operation` and `error_type`; provider messages and exception
123
+ chains are not logged. Successful calls and cancellation do not emit this warning;
124
+ cancellation still records an error span and metrics.
125
+
126
+ HTTP and httpx2 emit one `dependency.call_failed` warning per failed attempt (transport,
127
+ body or close exception, cancellation, or HTTP status >= 400). Fields are only `dependency`,
128
+ `operation` (normalized method) and `error_type`. HTTP rejection logs use `status_<code>`;
129
+ the metric still uses only its status-code label for that case. The logger runs within the
130
+ HTTP span so configured log processors can attach its trace/span IDs. Successful completed
131
+ attempts emit no log. Intentional partial close emits `dependency.stream_closed` at INFO
132
+ with dependency and operation; it is not an outage.
133
+
134
+ `log_failures=False` suppresses these HTTP attempt logs without disabling metrics or spans.
135
+ QStash, Resend and Kubernetes Jobs set this option because their logical operation already emits a failure
136
+ warning; they do not log the same failure at both levels. SDK retry loops can produce one
137
+ warning per failed attempt. HTTP counters measure attempts, not logical API/SDK calls.
138
+
139
+ The transport observes network bytes and connection cleanup. Parsing JSON, SSE events,
140
+ model output, content decoding above the raw stream, and application errors while processing
141
+ chunks belong to the caller/SDK operation. A 200 SSE response containing an application
142
+ error event is not detected by an HTTP transport.
143
+ S3 presigning and local AES work in KMS are outside remote-operation telemetry.
144
+
145
+ Instrumentation takes effect only when applications use these wrappers. The application
146
+ must also configure logging, exporters and shutdown/flush through 021-python-observability.
147
+ Defining a client in this package does not migrate existing SDK usage or guarantee that
148
+ telemetry reaches an external backend.
149
+
150
+ ## Trace propagation
151
+
152
+ - HTTP (`HttpClient`, `http.httpx.ObservedTransport`): the current `traceparent` and
153
+ `tracestate` replace the caller's trace pair, and a bound `x-request-id` is sent.
154
+ A native httpx transport can disable injection with `propagate=False`.
155
+ - httpx2 (`ObservedTransport`): no context is injected unless `propagate=True`.
156
+ With injection disabled, caller-supplied headers are left as supplied.
157
+ When enabled, both caller-supplied trace headers are replaced with the current context.
158
+ - Resend: nothing is sent. The `traceparent`, `tracestate` and `x-request-id` headers that
159
+ the HTTP transport adds are removed before the request leaves.
160
+ - SES, KMS, Secrets Manager, Bedrock and Vercel Blob: nothing is sent by the default transports.
161
+ - SQS: `traceparent` and `tracestate` are message attributes; `ReceivedMessage.trace_context()`
162
+ returns the producer's context. Caller-supplied values for both trace attributes are
163
+ replaced with the current context.
164
+ - QStash: the `publish` span's context is sent as `Upstash-Forward-traceparent` and
165
+ `Upstash-Forward-tracestate` for delivery to the consumer. `propagate=False` removes
166
+ both forwarded trace headers. Caller values never override them. The request to QStash
167
+ itself still uses the HTTP transport's propagation policy.
168
+
169
+ ## Errors
170
+
171
+ `DependencyUnavailable(dependency, operation, reason)` with `reason` one of `timeout`,
172
+ `connection_error`, `throttled`, `status_<5xx>`. Raised only when retrying later may work.
173
+ `HttpClient.request`: only after the last attempt; HTTP statuses are returned, never raised.
174
+ `HttpClient.stream`: exactly one attempt; transport/body failures use the same conversion,
175
+ with no replay after headers or a partial body. Native httpx/httpx2 `ObservedTransport`
176
+ instances preserve SDK exceptions and never retry.
177
+ HTTP local protocol/unsupported protocol errors and Redis authentication
178
+ or authorization errors retain their native type and are not treated as outages.
179
+
180
+ QStash maps HTTP 429 to `throttled`, 5xx to `status_<code>`, and transport outages to
181
+ `timeout` or `connection_error`, with operation `publish`. Other non-2xx responses raise
182
+ `QStashResponseError(status_code, reason="rejected")`. A 2xx response must contain a
183
+ non-empty `messageId` and, if present, a boolean `deduplicated`; otherwise it raises
184
+ `QStashResponseError` with `reason="invalid_response"`. A missing deduplication flag
185
+ is inferred from status 202. Neither error includes the provider response body.
186
+ `PublishedMessage(message_id, deduplicated)` represents acceptance, not completed delivery.
187
+
188
+ The exception's message and fields contain only the static dependency, operation and reason.
189
+ The chained provider exception is preserved for debugging and is not sanitized.
190
+
191
+ Email: `EmailRejected(dependency, status, code)` when the provider refused the message or
192
+ the credentials, for both senders. `status` is the HTTP status; `code` is Resend's error
193
+ `name` or the SES error code, reduced to `unknown` unless it is a short identifier. Its
194
+ message is `<dependency> send: rejected (<status> <code>)`; never the provider's message.
195
+ Resend: 429 is `throttled`, 5xx is `status_<5xx>`, transport timeouts and connection
196
+ failures are `timeout` / `connection_error`; every other non-2xx is `EmailRejected` (409
197
+ `concurrent_idempotent_requests` included: the first request is still running). SES: the
198
+ shared AWS classification, and every other `ClientError` becomes `EmailRejected` chained to
199
+ it. Resend's request is sent once; `idempotency_key` becomes its `Idempotency-Key` header
200
+ (1–256 characters, kept by Resend for 24 hours). SES has no idempotency key: the argument is
201
+ validated and ignored.
202
+
203
+ KMS: throttling, 5xx (`KMSInternalException`, `DependencyTimeoutException`,
204
+ `KeyUnavailableException`), timeouts and connection failures are `DependencyUnavailable`.
205
+ Access denied, disabled or missing keys, invalid key state, a wrong encryption context or a
206
+ tampered ciphertext (`InvalidCiphertextException`) and a ciphertext from another key
207
+ (`IncorrectKeyException`) keep their botocore type. `decrypt` always names the client's key.
208
+ Every call requires a non-empty encryption context. `unseal` raises `ValueError` for a
209
+ malformed envelope and `cryptography.exceptions.InvalidTag` when the payload was altered.
210
+
211
+ Secrets Manager: throttling, 5xx (`InternalServiceError`), timeouts and connection failures
212
+ are `DependencyUnavailable`. A missing secret (`ResourceNotFoundException`), access denied,
213
+ a value KMS cannot decrypt (`DecryptionFailure`), a secret scheduled for deletion
214
+ (`InvalidRequestException`) and invalid parameters keep their botocore type. `get` is a read:
215
+ retrying it is safe. A version with neither `SecretString` nor `SecretBinary` raises
216
+ `ValueError`.
217
+
218
+ Bedrock: throttling (`ThrottlingException`, and `throttlingException` inside an event stream),
219
+ 5xx, `ModelNotReadyException` (`throttled`), the stream errors `serviceUnavailableException`,
220
+ `internalServerException` and `modelStreamErrorException`, timeouts and connection failures
221
+ are `DependencyUnavailable`. Access denied, validation errors, a missing model or profile and
222
+ quota limits (`ServiceQuotaExceededException`) keep their botocore type. Leaving a stream
223
+ early or calling its `close()` ends the operation as `ok` and logs `dependency.stream_closed`
224
+ at INFO. A model call is not idempotent in cost: botocore's retries bill each attempt.
225
+
226
+ Kubernetes Jobs: 429 is `throttled`, 5xx `status_<5xx>`, transport timeouts and connection
227
+ failures `timeout` / `connection_error`, all `DependencyUnavailable`. A 409 whose reason is
228
+ `AlreadyExists` on `create` and a 404 on `get` or `delete` are results, not errors. Any other
229
+ non-success status raises `KubernetesApiError(status_code, reason)`, where `reason` is the
230
+ Status object's machine-readable reason (`Forbidden`, `Invalid`, `Conflict`, ...) or
231
+ `Unknown`; a 201 whose name does not match raises it with `InvalidResponse`. POST is sent
232
+ once; GET and DELETE retry 502/503/504 and network failures once. Local validation (DNS-label
233
+ names and namespace, `metadata.name` required, `generateName` refused, `kind` must be Job)
234
+ happens before any request.
235
+
236
+ Vercel Blob (`clients_021.storage.vercel_blob`): HTTP 429 maps to `throttled`; 5xx to
237
+ `status_<code>`; HTTP timeouts and total deadlines to `timeout`; network/remote protocol
238
+ failures to `connection_error`. Known transport causes wrapped by the SDK are classified
239
+ without inspecting provider messages. Other errors retain their SDK type, including
240
+ `BlobAccessError`, `BlobStoreNotFoundError`, `BlobPreconditionFailedError`, and
241
+ `BlobStreamError` for malformed acknowledgements. Only `BlobNotFoundError` becomes `None`
242
+ for `get`/`head`, counted as success. `put` returns the SDK's `PutResult` (exported as
243
+ `BlobUpload`); `head` returns its `HeadResult` (`BlobInfo`). These types are provider-specific.
244
+ SDK validation and credential failures can produce an error span but perform no network I/O.
245
+
246
+ ## Sealed envelope format
247
+
248
+ `KmsClient.seal` output is stored data, so its layout is part of this contract: version
249
+ byte `0x01`, wrapped data key length (2 bytes, big endian), the wrapped key (KMS
250
+ `GenerateDataKey`, `AES_256`, same encryption context), a 12-byte random nonce, then the
251
+ AES-256-GCM ciphertext and 16-byte tag. The GCM additional data is the header (version,
252
+ length and wrapped key) followed by the encryption context as compact JSON with sorted keys.
253
+ A new data key is generated for every envelope.
254
+
255
+ ## Lifecycle and retry limits
256
+
257
+ HTTP, Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock and Kubernetes
258
+ Jobs expose `aclose()` (Bedrock also `close()`). Close them after
259
+ in-flight calls complete; the wrapper owns the underlying client, including one supplied
260
+ through the constructor.
261
+ AWS factories configure three total attempts; injected clients retain their own policy.
262
+ Cancellation of an AWS await does not cancel its worker thread or undo a remote write.
263
+ A transient failure does not guarantee that replaying a non-idempotent operation is safe.
264
+
265
+ QStash publishes once, never automatically replays a POST, and preserves the required
266
+ deduplication key exactly. Keys containing `:` fail locally, matching the provider
267
+ restriction without lossy normalization. Delivery retries, delay and delivery timeout are separate
268
+ provider options. The publication timeout is mandatory and finite. Consumers remain
269
+ responsible for idempotency and signature verification. A closed QStash client cannot
270
+ publish again.
271
+
272
+ `HttpClient.request` and `HttpClient.stream` accept at most one of `json`, `form` or raw byte `content`. Passing
273
+ more than one non-None body format raises `ValueError` before sending a request.
274
+
275
+ `HttpClient.stream` is an async context manager yielding a native httpx response; it does
276
+ not buffer the response and also accepts an async iterable of upload bytes. Use `async with`
277
+ and `aiter_bytes`, `aiter_lines` or `aiter_raw`. SDK callers using manual `send(stream=True)`
278
+ must eventually call `response.aclose()`; an abandoned response cannot finalize its
279
+ telemetry or free its connection. Client shutdown does not replace closing active responses.
280
+ Close responses and finish consumers before closing the client/Observability.
281
+
282
+ Body resources close once on EOF, read failure or explicit close. Cleanup is shielded from
283
+ cancellation and awaited; cancellation is then re-raised. A secondary cleanup failure does
284
+ not replace a read failure. These async clients run on asyncio. Custom transports must implement a terminating `aclose()`.
285
+ Timeouts remain per network I/O, not a total stream deadline. `HttpClient` closes an injected
286
+ transport once even when multiple dependency pools borrow it; closed clients cannot reopen.
287
+
288
+ Vercel Blob uses the optional `vercel-blob>=0.5,<0.6` SDK and explicit read-write credentials,
289
+ store ID and access mode. It owns a shared transport; each operation borrows it in a separate
290
+ SDK session. No environment credentials or global SDK client perform requests. The total
291
+ operation deadline defaults to 10 seconds and includes buffered downloads. Timeout and
292
+ cancellation do not imply a remote write was undone. Neither this wrapper nor the SDK
293
+ retries operations; an injected transport must respect this policy. Overwrite and random
294
+ suffixes default to false. Callers decide whether a repeated write is safe. A closed client
295
+ cannot reopen its pool. No additional HTTP metrics are emitted by this client.
296
+
297
+ ## Migration from 0.2.x
298
+
299
+ This is a breaking telemetry change, released with the next pre-1.0 minor version. HTTP
300
+ metric names and span names stay the same, but their durations now include bodies, consumer
301
+ backpressure and close; counts are emitted at completion instead of at headers. Existing
302
+ series gain `http.response.body.complete`. Revisit latency thresholds, filter/aggregate the
303
+ new label and expect long-lived streams to report only when they complete or close. Native
304
+ transport logs are now enabled by default; use the explicit opt-out when an enclosing
305
+ logical operation already owns the warning. Do not add a second observed transport around
306
+ these clients, or each request would be counted twice.
307
+
308
+ ## Never recorded
309
+
310
+ Keys, URLs, query strings, bodies, message ids, receipts, object names, bucket or queue
311
+ names, provider error messages. Email: sender, recipient and reply-to addresses, subjects,
312
+ text and HTML bodies, tags, idempotency keys, API keys, configuration set names. KMS: key
313
+ ids, ARNs and aliases, encryption contexts, plaintext, ciphertext and data keys. Secrets
314
+ Manager: secret ids, names and ARNs, version ids, version stages and secret values. Bedrock:
315
+ model ids, inference profile ARNs, prompts, system prompts, outputs, tool arguments and
316
+ results, request ids and guardrail details. Kubernetes Jobs: namespaces, Job names, labels,
317
+ manifests, API server addresses, service account tokens and Status messages.
318
+
319
+ Vercel Blob: read-write tokens, store IDs, pathnames, delivery URLs, content and ETags.
@@ -8,23 +8,27 @@ not need to be reimplemented here.
8
8
  ## Layout and public API
9
9
 
10
10
  Group domain clients by capability, with a module per provider even when the package
11
- currently has one provider. HTTP transports are cross-cutting infrastructure and stay at
12
- the root. Tests stay in `tests/test_<provider>.py`.
11
+ currently has one provider. HTTP implementations live under `http/`, with explicit
12
+ imports for optional transports. Tests stay in `tests/test_<provider>.py`.
13
13
 
14
14
  ```text
15
15
  clients_021/
16
16
  email/ # _message.py (shared protocol/types), resend.py, ses.py
17
17
  queue/ # sqs.py, qstash.py
18
- http.py
19
- httpx2.py
18
+ http/ # httpx.py (HttpClient/ObservedTransport), httpx2.py (ObservedTransport)
20
19
  cache/ # redis.py
21
20
  storage/ # s3.py, vercel_blob.py
22
21
  crypto/ # kms.py
22
+ secrets/ # secrets_manager.py
23
+ llm/ # bedrock.py
24
+ compute/ # kubernetes.py (over the shared HTTP client, no extra)
23
25
  ```
24
26
 
25
27
  The original `clients_021.redis`, `clients_021.s3` and `clients_021.kms` imports remain
26
28
  compatibility re-exports of the same classes and result types. New code uses the capability
27
- paths. Do not remove compatibility modules without a breaking release.
29
+ paths. `clients_021.http` re-exports its existing types from `http.httpx`, and the original
30
+ `clients_021.httpx2` remains an alias for `http.httpx2`. Keep `clients_021.HttpClient`
31
+ available too. Do not remove compatibility modules without a breaking release.
28
32
 
29
33
  Public result and exception types live beside their provider unless they are truly shared.
30
34
  A common protocol requires interchangeable operations and semantics: `EmailSender.send`
@@ -61,10 +65,16 @@ Each implementation must make these decisions explicit in README.md and CONTRACT
61
65
  | Telemetry | Choose a metric family, fixed operations and system attribute. Never record URLs, credentials, bodies, object names or request-derived ids. |
62
66
  | Propagation | State the default and opt-out. Replace `traceparent` and `tracestate` together, including removing stale state. |
63
67
 
64
- Use `_observe.observed` for a logical cache, messaging, storage or crypto operation. HTTP-backed
68
+ Use `_observe.observed` for a logical cache, messaging, storage or crypto operation, and
69
+ `_observe.Call` for synchronous SDK calls or operations that end after the call returns
70
+ (event streams). HTTP-backed
65
71
  clients may also emit the transport's HTTP span/metrics: one logical operation and one
66
72
  network request are different measurements. Do not sum those counters as distinct
67
- business operations. Disable exception recording when provider errors can contain data.
73
+ business operations. Disable nested HTTP attempt logs with `log_failures=False` when the
74
+ logical operation owns the failure log. HTTP transport duration includes raw-body consumption
75
+ and resource close; keep measurement once-only, incremental, and detached from consumer
76
+ context between chunks. Test read/close errors, cancellation (including repeated cancellation
77
+ while closing), early close, and cross-task consumption. Disable exception recording when provider errors can contain data.
68
78
  The safe wrapper message does not make a native chained exception safe to log.
69
79
 
70
80
  Expose small, stable results when the caller needs them. Validate acknowledgements before