021-python-clients 0.2.0__tar.gz → 0.3.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.
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/CHANGELOG.md +20 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/CONTRACT.md +91 -13
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/CONTRIBUTING.md +11 -6
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/PKG-INFO +58 -15
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/README.md +57 -14
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/pyproject.toml +1 -1
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/__init__.py +1 -1
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/email/resend.py +5 -1
- 021_python_clients-0.3.0/src/clients_021/http/__init__.py +11 -0
- 021_python_clients-0.3.0/src/clients_021/http/_observe.py +175 -0
- 021_python_clients-0.3.0/src/clients_021/http/httpx.py +280 -0
- 021_python_clients-0.3.0/src/clients_021/http/httpx2.py +76 -0
- 021_python_clients-0.3.0/src/clients_021/httpx2.py +5 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/queue/qstash.py +1 -1
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_contract.py +1 -1
- 021_python_clients-0.3.0/tests/test_http_stream_lifecycle.py +270 -0
- 021_python_clients-0.3.0/tests/test_http_streaming.py +482 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_httpx2.py +8 -2
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/wheel_minimal.py +11 -1
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/wheel_smoke.py +12 -3
- 021_python_clients-0.2.0/src/clients_021/http.py +0 -131
- 021_python_clients-0.2.0/src/clients_021/httpx2.py +0 -93
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/.gitignore +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/LICENSE +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/_aws.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/_observe.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/cache/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/cache/redis.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/crypto/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/crypto/kms.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/email/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/email/_message.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/email/ses.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/errors.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/kms.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/py.typed +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/queue/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/queue/sqs.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/redis.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/s3.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/storage/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/storage/s3.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/src/clients_021/storage/vercel_blob.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/__init__.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/conftest.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/qstash_local.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_email.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_http.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_kms.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_qstash.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_redis.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_s3.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_ses.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_sqs.py +0 -0
- {021_python_clients-0.2.0 → 021_python_clients-0.3.0}/tests/test_vercel_blob.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are recorded here by release-please from Conventional Commits.
|
|
4
4
|
|
|
5
|
+
## [0.3.0](https://github.com/from021-io/python-clients/compare/v0.2.0...v0.3.0) (2026-10-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* **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.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **http:** observe complete response bodies and streams ([333cece](https://github.com/from021-io/python-clients/commit/333cece69c41a9ef16828f83e303be4183a5a187))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Refactors
|
|
18
|
+
|
|
19
|
+
* **http:** group clients and transports by capability ([1a7b41a](https://github.com/from021-io/python-clients/commit/1a7b41a5dba214fb109acccc9e3e08a9d123e3ee))
|
|
20
|
+
|
|
5
21
|
## [0.2.0](https://github.com/from021-io/python-clients/compare/v0.1.0...v0.2.0) (2026-10-21)
|
|
6
22
|
|
|
7
23
|
|
|
@@ -22,10 +38,14 @@ All notable changes are recorded here by release-please from Conventional Commit
|
|
|
22
38
|
|
|
23
39
|
### Added
|
|
24
40
|
|
|
41
|
+
- Add `HttpClient.stream` and a native httpx `ObservedTransport` for incremental bodies with explicit lifetime and no streaming retries.
|
|
42
|
+
|
|
25
43
|
- Add an optional Vercel Blob client with observed CRUD, explicit public/private access, bounded operations and SDK-native metadata/errors.
|
|
26
44
|
|
|
27
45
|
### Changed
|
|
28
46
|
|
|
47
|
+
- **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.
|
|
48
|
+
|
|
29
49
|
- Group Redis, S3 and KMS under cache, storage and crypto, retaining the 0.1.0 imports as compatibility aliases.
|
|
30
50
|
|
|
31
51
|
## 0.1.0 (2026-10-21)
|
|
@@ -19,7 +19,11 @@ Redis lives in `clients_021.cache.redis`, S3 in `clients_021.storage.s3` and KMS
|
|
|
19
19
|
`clients_021.crypto.kms`. The 0.1.0 root modules (`redis`, `s3`, `kms`) continue to re-export
|
|
20
20
|
the same clients and result types (`ObjectInfo`, `DataKey`). Existing call signatures,
|
|
21
21
|
metric/span names and error semantics are unchanged. Capability package imports do not
|
|
22
|
-
load optional SDKs. HTTP
|
|
22
|
+
load optional SDKs. HTTP implementations live in `clients_021.http.httpx` and
|
|
23
|
+
`clients_021.http.httpx2`. `clients_021.http` retains `HttpClient`, `HttpResponse`,
|
|
24
|
+
`HttpMethod`, `IDEMPOTENT_METHODS` and `RETRY_STATUS`; the root `clients_021` exports remain
|
|
25
|
+
unchanged. `clients_021.httpx2.ObservedTransport` re-exports the same class as the new path.
|
|
26
|
+
Importing `clients_021.http` does not require the `httpx2` extra.
|
|
23
27
|
|
|
24
28
|
## Metrics
|
|
25
29
|
|
|
@@ -36,7 +40,7 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
36
40
|
| `storage.client.operation.duration` | Histogram / `s` | same | S3, Vercel Blob |
|
|
37
41
|
| `crypto.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | KMS |
|
|
38
42
|
| `crypto.client.operation.duration` | Histogram / `s` | same | KMS |
|
|
39
|
-
| `http.client.request.count` | Counter / `{request}` | dependency, http.request.method, http.response.status_code, error.type | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
|
|
43
|
+
| `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) |
|
|
40
44
|
| `http.client.request.duration` | Histogram / `s` | same | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
|
|
41
45
|
|
|
42
46
|
- `status`: `ok` or `error`. `error.type` only on errors.
|
|
@@ -52,7 +56,7 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
52
56
|
`NotFoundException`, for KMS).
|
|
53
57
|
- Resend records two levels: the `send` operation (`messaging.client.operation.*`) and,
|
|
54
58
|
inside it, its HTTP request (`http.client.request.*`), both under the same `dependency`.
|
|
55
|
-
- HTTP transport metrics
|
|
59
|
+
- HTTP transport metrics use the bounded labels (`timeout`, `cancelled`,
|
|
56
60
|
`connection_error` or `internal_error`). These describe the transport exception, while
|
|
57
61
|
`DependencyUnavailable.reason` describes the failure returned to the caller.
|
|
58
62
|
- httpx2 uses `timeout`, `connection_error`, `cancelled`, or the exception class name.
|
|
@@ -70,21 +74,69 @@ metric labels plus `db.system.name=redis`, `messaging.system=aws_sqs`,
|
|
|
70
74
|
Resend's `HTTP <dependency>` span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
|
|
71
75
|
status ≥ 400).
|
|
72
76
|
Cancellation propagates unchanged and records ERROR with `error.type=cancelled`.
|
|
73
|
-
HTTP transports measure
|
|
77
|
+
HTTP transports measure from sending the request through response-body EOF and resource
|
|
78
|
+
close, or explicit early close. Uploads and incremental downloads are included, as are
|
|
79
|
+
consumer pauses between chunks and cleanup latency. Each attempt ends its span and emits
|
|
80
|
+
one count and one duration exactly once. An HTTP 200 followed by a body timeout is ERROR
|
|
81
|
+
with status code 200 and `error.type=timeout`; the status code describes the headers only.
|
|
82
|
+
|
|
83
|
+
`http.response.body.complete` is a boolean on HTTP spans and metric points: true after
|
|
84
|
+
response-body EOF, false when no response arrived or it was closed before EOF. It does not
|
|
85
|
+
imply success: an HTTP rejection or close failure can still set ERROR. An intentional
|
|
86
|
+
partial close without an error does not set ERROR. Cancellation sets `error.type=cancelled`,
|
|
87
|
+
including cancellation while reading, closing or waiting between chunks in a stream context.
|
|
88
|
+
|
|
89
|
+
The HTTP span is current only while sending, reading, closing and emitting its log. It is
|
|
90
|
+
not left attached while a caller processes a chunk; interleaved and cross-task consumers
|
|
91
|
+
keep their own context. All HTTP attempt spans retain the parent active at request start.
|
|
74
92
|
|
|
75
93
|
QStash adds one logical `publish` operation around the HTTP transport request. The
|
|
76
94
|
messaging duration includes reading and validating the acknowledgement; the nested HTTP
|
|
77
|
-
span
|
|
95
|
+
span includes reading and closing the response body. Both use the configured static dependency name.
|
|
78
96
|
Local input validation occurs before either operation is recorded.
|
|
79
97
|
|
|
98
|
+
## Logging and coverage limits
|
|
99
|
+
|
|
100
|
+
Redis, SQS, QStash, S3, Vercel Blob, Resend, SES and KMS emit a structured
|
|
101
|
+
`dependency.call_failed` warning when an observed remote operation raises an exception.
|
|
102
|
+
Fields are `dependency`, `operation` and `error_type`; provider messages and exception
|
|
103
|
+
chains are not logged. Successful calls and cancellation do not emit this warning;
|
|
104
|
+
cancellation still records an error span and metrics.
|
|
105
|
+
|
|
106
|
+
HTTP and httpx2 emit one `dependency.call_failed` warning per failed attempt (transport,
|
|
107
|
+
body or close exception, cancellation, or HTTP status >= 400). Fields are only `dependency`,
|
|
108
|
+
`operation` (normalized method) and `error_type`. HTTP rejection logs use `status_<code>`;
|
|
109
|
+
the metric still uses only its status-code label for that case. The logger runs within the
|
|
110
|
+
HTTP span so configured log processors can attach its trace/span IDs. Successful completed
|
|
111
|
+
attempts emit no log. Intentional partial close emits `dependency.stream_closed` at INFO
|
|
112
|
+
with dependency and operation; it is not an outage.
|
|
113
|
+
|
|
114
|
+
`log_failures=False` suppresses these HTTP attempt logs without disabling metrics or spans.
|
|
115
|
+
QStash and Resend set this option because their logical operation already emits a failure
|
|
116
|
+
warning; they do not log the same failure at both levels. SDK retry loops can produce one
|
|
117
|
+
warning per failed attempt. HTTP counters measure attempts, not logical API/SDK calls.
|
|
118
|
+
|
|
119
|
+
The transport observes network bytes and connection cleanup. Parsing JSON, SSE events,
|
|
120
|
+
model output, content decoding above the raw stream, and application errors while processing
|
|
121
|
+
chunks belong to the caller/SDK operation. A 200 SSE response containing an application
|
|
122
|
+
error event is not detected by an HTTP transport.
|
|
123
|
+
S3 presigning and local AES work in KMS are outside remote-operation telemetry.
|
|
124
|
+
|
|
125
|
+
Instrumentation takes effect only when applications use these wrappers. The application
|
|
126
|
+
must also configure logging, exporters and shutdown/flush through 021-python-observability.
|
|
127
|
+
Defining a client in this package does not migrate existing SDK usage or guarantee that
|
|
128
|
+
telemetry reaches an external backend.
|
|
129
|
+
|
|
80
130
|
## Trace propagation
|
|
81
131
|
|
|
82
|
-
- HTTP (`HttpClient`): `traceparent
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
- HTTP (`HttpClient`, `http.httpx.ObservedTransport`): the current `traceparent` and
|
|
133
|
+
`tracestate` replace the caller's trace pair, and a bound `x-request-id` is sent.
|
|
134
|
+
A native httpx transport can disable injection with `propagate=False`.
|
|
135
|
+
- httpx2 (`ObservedTransport`): no context is injected unless `propagate=True`.
|
|
136
|
+
With injection disabled, caller-supplied headers are left as supplied.
|
|
85
137
|
When enabled, both caller-supplied trace headers are replaced with the current context.
|
|
86
138
|
- Resend: nothing is sent. The `traceparent`, `tracestate` and `x-request-id` headers that
|
|
87
|
-
|
|
139
|
+
the HTTP transport adds are removed before the request leaves.
|
|
88
140
|
- SES, KMS and Vercel Blob: nothing is sent by the default transports.
|
|
89
141
|
- SQS: `traceparent` and `tracestate` are message attributes; `ReceivedMessage.trace_context()`
|
|
90
142
|
returns the producer's context. Caller-supplied values for both trace attributes are
|
|
@@ -98,10 +150,12 @@ Local input validation occurs before either operation is recorded.
|
|
|
98
150
|
|
|
99
151
|
`DependencyUnavailable(dependency, operation, reason)` with `reason` one of `timeout`,
|
|
100
152
|
`connection_error`, `throttled`, `status_<5xx>`. Raised only when retrying later may work.
|
|
101
|
-
|
|
153
|
+
`HttpClient.request`: only after the last attempt; HTTP statuses are returned, never raised.
|
|
154
|
+
`HttpClient.stream`: exactly one attempt; transport/body failures use the same conversion,
|
|
155
|
+
with no replay after headers or a partial body. Native httpx/httpx2 `ObservedTransport`
|
|
156
|
+
instances preserve SDK exceptions and never retry.
|
|
102
157
|
HTTP local protocol/unsupported protocol errors and Redis authentication
|
|
103
|
-
or authorization errors retain their native type and are not treated as outages.
|
|
104
|
-
transports always preserve the native exception for the owning SDK.
|
|
158
|
+
or authorization errors retain their native type and are not treated as outages.
|
|
105
159
|
|
|
106
160
|
QStash maps HTTP 429 to `throttled`, 5xx to `status_<code>`, and transport outages to
|
|
107
161
|
`timeout` or `connection_error`, with operation `publish`. Other non-2xx responses raise
|
|
@@ -169,9 +223,22 @@ provider options. The publication timeout is mandatory and finite. Consumers rem
|
|
|
169
223
|
responsible for idempotency and signature verification. A closed QStash client cannot
|
|
170
224
|
publish again.
|
|
171
225
|
|
|
172
|
-
`HttpClient.request`
|
|
226
|
+
`HttpClient.request` and `HttpClient.stream` accept at most one of `json`, `form` or raw byte `content`. Passing
|
|
173
227
|
more than one non-None body format raises `ValueError` before sending a request.
|
|
174
228
|
|
|
229
|
+
`HttpClient.stream` is an async context manager yielding a native httpx response; it does
|
|
230
|
+
not buffer the response and also accepts an async iterable of upload bytes. Use `async with`
|
|
231
|
+
and `aiter_bytes`, `aiter_lines` or `aiter_raw`. SDK callers using manual `send(stream=True)`
|
|
232
|
+
must eventually call `response.aclose()`; an abandoned response cannot finalize its
|
|
233
|
+
telemetry or free its connection. Client shutdown does not replace closing active responses.
|
|
234
|
+
Close responses and finish consumers before closing the client/Observability.
|
|
235
|
+
|
|
236
|
+
Body resources close once on EOF, read failure or explicit close. Cleanup is shielded from
|
|
237
|
+
cancellation and awaited; cancellation is then re-raised. A secondary cleanup failure does
|
|
238
|
+
not replace a read failure. These async clients run on asyncio. Custom transports must implement a terminating `aclose()`.
|
|
239
|
+
Timeouts remain per network I/O, not a total stream deadline. `HttpClient` closes an injected
|
|
240
|
+
transport once even when multiple dependency pools borrow it; closed clients cannot reopen.
|
|
241
|
+
|
|
175
242
|
Vercel Blob uses the optional `vercel-blob>=0.5,<0.6` SDK and explicit read-write credentials,
|
|
176
243
|
store ID and access mode. It owns a shared transport; each operation borrows it in a separate
|
|
177
244
|
SDK session. No environment credentials or global SDK client perform requests. The total
|
|
@@ -181,6 +248,17 @@ retries operations; an injected transport must respect this policy. Overwrite an
|
|
|
181
248
|
suffixes default to false. Callers decide whether a repeated write is safe. A closed client
|
|
182
249
|
cannot reopen its pool. No additional HTTP metrics are emitted by this client.
|
|
183
250
|
|
|
251
|
+
## Migration from 0.2.x
|
|
252
|
+
|
|
253
|
+
This is a breaking telemetry change, released with the next pre-1.0 minor version. HTTP
|
|
254
|
+
metric names and span names stay the same, but their durations now include bodies, consumer
|
|
255
|
+
backpressure and close; counts are emitted at completion instead of at headers. Existing
|
|
256
|
+
series gain `http.response.body.complete`. Revisit latency thresholds, filter/aggregate the
|
|
257
|
+
new label and expect long-lived streams to report only when they complete or close. Native
|
|
258
|
+
transport logs are now enabled by default; use the explicit opt-out when an enclosing
|
|
259
|
+
logical operation already owns the warning. Do not add a second observed transport around
|
|
260
|
+
these clients, or each request would be counted twice.
|
|
261
|
+
|
|
184
262
|
## Never recorded
|
|
185
263
|
|
|
186
264
|
Keys, URLs, query strings, bodies, message ids, receipts, object names, bucket or queue
|
|
@@ -8,15 +8,14 @@ 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
|
|
12
|
-
|
|
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
|
|
@@ -24,7 +23,9 @@ clients_021/
|
|
|
24
23
|
|
|
25
24
|
The original `clients_021.redis`, `clients_021.s3` and `clients_021.kms` imports remain
|
|
26
25
|
compatibility re-exports of the same classes and result types. New code uses the capability
|
|
27
|
-
paths.
|
|
26
|
+
paths. `clients_021.http` re-exports its existing types from `http.httpx`, and the original
|
|
27
|
+
`clients_021.httpx2` remains an alias for `http.httpx2`. Keep `clients_021.HttpClient`
|
|
28
|
+
available too. Do not remove compatibility modules without a breaking release.
|
|
28
29
|
|
|
29
30
|
Public result and exception types live beside their provider unless they are truly shared.
|
|
30
31
|
A common protocol requires interchangeable operations and semantics: `EmailSender.send`
|
|
@@ -64,7 +65,11 @@ Each implementation must make these decisions explicit in README.md and CONTRACT
|
|
|
64
65
|
Use `_observe.observed` for a logical cache, messaging, storage or crypto operation. HTTP-backed
|
|
65
66
|
clients may also emit the transport's HTTP span/metrics: one logical operation and one
|
|
66
67
|
network request are different measurements. Do not sum those counters as distinct
|
|
67
|
-
business operations. Disable
|
|
68
|
+
business operations. Disable nested HTTP attempt logs with `log_failures=False` when the
|
|
69
|
+
logical operation owns the failure log. HTTP transport duration includes raw-body consumption
|
|
70
|
+
and resource close; keep measurement once-only, incremental, and detached from consumer
|
|
71
|
+
context between chunks. Test read/close errors, cancellation (including repeated cancellation
|
|
72
|
+
while closing), early close, and cross-task consumption. Disable exception recording when provider errors can contain data.
|
|
68
73
|
The safe wrapper message does not make a native chained exception safe to log.
|
|
69
74
|
|
|
70
75
|
Expose small, stable results when the caller needs them. Validate acknowledgements before
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 021-python-clients
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email and KMS, built on 021-python-observability
|
|
5
5
|
Project-URL: Repository, https://github.com/from021-io/python-clients
|
|
6
6
|
Project-URL: Changelog, https://github.com/from021-io/python-clients/blob/main/CHANGELOG.md
|
|
@@ -48,8 +48,8 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
|
|
|
48
48
|
|
|
49
49
|
| Module | Extra | What it gives |
|
|
50
50
|
| --- | --- | --- |
|
|
51
|
-
| `clients_021.http` | none | `HttpClient`:
|
|
52
|
-
| `clients_021.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
51
|
+
| `clients_021.http.httpx` | none | `HttpClient`: buffered requests, streaming and full-body telemetry; native `ObservedTransport` |
|
|
52
|
+
| `clients_021.http.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
53
53
|
| `clients_021.cache.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
54
54
|
| `clients_021.queue.qstash` | none | `QStashClient`: JSON publishing, deduplication, delivery options and forwarded trace context |
|
|
55
55
|
| `clients_021.queue.sqs` | `aws` | `SqsClient`: send, receive, delete, visibility, trace carried in the message |
|
|
@@ -66,10 +66,12 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
66
66
|
|
|
67
67
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
68
68
|
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
69
|
-
contains Redis; `crypto/` contains KMS.
|
|
70
|
-
explicitly:
|
|
69
|
+
contains Redis; `crypto/` contains KMS. `http/` contains the httpx client and httpx2
|
|
70
|
+
transport. Import providers explicitly:
|
|
71
71
|
|
|
72
72
|
```python
|
|
73
|
+
from clients_021.http import HttpClient
|
|
74
|
+
from clients_021.http.httpx2 import ObservedTransport # requires the httpx2 extra
|
|
73
75
|
from clients_021.queue.sqs import SqsClient
|
|
74
76
|
from clients_021.queue.qstash import QStashClient
|
|
75
77
|
from clients_021.storage.s3 import S3Client
|
|
@@ -85,7 +87,9 @@ does not load the optional AWS SDK.
|
|
|
85
87
|
|
|
86
88
|
The 0.1.0 imports `clients_021.redis`, `clients_021.s3` and `clients_021.kms` remain
|
|
87
89
|
compatible aliases for the same classes and result types. Importing a capability package
|
|
88
|
-
does not load its optional SDKs.
|
|
90
|
+
does not load its optional SDKs. `clients_021.HttpClient` and `clients_021.http` keep their
|
|
91
|
+
published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
|
|
92
|
+
`clients_021.http.httpx2.ObservedTransport`.
|
|
89
93
|
|
|
90
94
|
## What every call gets
|
|
91
95
|
|
|
@@ -97,7 +101,8 @@ does not load its optional SDKs.
|
|
|
97
101
|
`crypto.client.operation.*` (KMS), `http.client.request.*` (HTTP, and the HTTP request
|
|
98
102
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
99
103
|
- Redis, SQS, QStash, S3, Vercel Blob, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
100
|
-
with dependency, operation and error type only. HTTP
|
|
104
|
+
with dependency, operation and error type only. HTTP/httpx2 also log failed attempts,
|
|
105
|
+
including body failures and cancellation; QStash/Resend suppress the nested HTTP log.
|
|
101
106
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
102
107
|
refused the connection, throttled or failed with a 5xx. Map it to a 503. Errors that mean
|
|
103
108
|
the request is wrong (access denied, a missing bucket, a wrong Redis type) keep their
|
|
@@ -290,21 +295,58 @@ ciphertext keep their botocore type (they are bugs or attacks, not outages); a t
|
|
|
290
295
|
envelope raises `cryptography`'s `InvalidTag`. The envelope layout is in
|
|
291
296
|
[CONTRACT.md](CONTRACT.md).
|
|
292
297
|
|
|
298
|
+
### HTTP bodies and streaming
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from clients_021.http import HttpClient
|
|
302
|
+
|
|
303
|
+
http = HttpClient(obs, timeout_seconds=10)
|
|
304
|
+
try:
|
|
305
|
+
async with http.stream("GET", download_url, dependency="documents") as response:
|
|
306
|
+
response.raise_for_status()
|
|
307
|
+
async for chunk in response.aiter_bytes():
|
|
308
|
+
await write_chunk(chunk)
|
|
309
|
+
finally:
|
|
310
|
+
await http.aclose()
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
These async clients run on asyncio. `request()` returns a buffered `HttpResponse`; `stream()` yields a native httpx response
|
|
314
|
+
without buffering and accepts an async iterable as upload `content`. Streaming sends once,
|
|
315
|
+
even for GET. The buffered client's configured idempotent retries are unchanged. Close the
|
|
316
|
+
response context before closing the client. For SDKs using native httpx, inject
|
|
317
|
+
`clients_021.http.httpx.ObservedTransport`; httpx2 SDKs use the transport below.
|
|
318
|
+
|
|
319
|
+
Each HTTP attempt emits one span, counter and duration after body EOF and connection close,
|
|
320
|
+
or early close. This includes upload, download, pauses between chunks and cleanup. Body
|
|
321
|
+
errors after 200 headers are recorded as errors. `http.response.body.complete` distinguishes
|
|
322
|
+
EOF from partial/unread close; it does not mean the status or cleanup succeeded. Intentional
|
|
323
|
+
partial close is INFO `dependency.stream_closed`; exceptions, cancellation and HTTP >= 400
|
|
324
|
+
produce WARNING `dependency.call_failed`. No success log, URL, header, body or provider
|
|
325
|
+
error message is emitted. `log_failures=False` disables attempt logs when an enclosing
|
|
326
|
+
operation owns the warning, as QStash and Resend do.
|
|
327
|
+
|
|
328
|
+
These duration semantics and the completion label change the 0.2.x telemetry contract;
|
|
329
|
+
see [migration notes](CONTRACT.md#migration-from-02x) before updating dashboards or alerts.
|
|
330
|
+
|
|
293
331
|
### SDKs on httpx2
|
|
294
332
|
|
|
295
333
|
For an SDK on httpx2:
|
|
296
334
|
|
|
297
335
|
```python
|
|
298
336
|
from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient
|
|
299
|
-
from clients_021.httpx2 import ObservedTransport
|
|
337
|
+
from clients_021.http.httpx2 import ObservedTransport
|
|
300
338
|
|
|
301
339
|
transport = ObservedTransport(obs, dependency="anthropic")
|
|
302
340
|
claude = AsyncAnthropic(http_client=DefaultAsyncHttpxClient(transport=transport))
|
|
303
341
|
```
|
|
304
342
|
|
|
305
|
-
It does not
|
|
343
|
+
It does not inject trace context into third-party API requests unless `propagate=True`.
|
|
306
344
|
The transport preserves SDK exceptions so the SDK retains control of retries and error
|
|
307
|
-
handling; it does not convert them to `DependencyUnavailable`.
|
|
345
|
+
handling; it does not convert them to `DependencyUnavailable`. Its span stays open through
|
|
346
|
+
incremental response reads and close. Use the SDK's stream context manager, or close manual
|
|
347
|
+
responses explicitly; abandoning a stream leaves both its connection and span open. Caller
|
|
348
|
+
processing keeps its own trace context, including when streams are interleaved or consumed
|
|
349
|
+
in another task.
|
|
308
350
|
|
|
309
351
|
## Service integration
|
|
310
352
|
|
|
@@ -338,7 +380,7 @@ async with AsyncExitStack() as resources:
|
|
|
338
380
|
|
|
339
381
|
Retry policies are explicit:
|
|
340
382
|
|
|
341
|
-
- HTTP retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
383
|
+
- Buffered HTTP (`request`) retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
342
384
|
remote protocol/proxy connection failures and 502/503/504. `retries=2` means at most
|
|
343
385
|
three attempts.
|
|
344
386
|
POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
|
|
@@ -372,10 +414,11 @@ stop an SDK request already running in its thread. Timeouts are per SDK/network
|
|
|
372
414
|
not an overall deadline. Limit consumer concurrency and let in-flight work finish before
|
|
373
415
|
closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
|
|
374
416
|
|
|
375
|
-
HTTP
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
417
|
+
HTTP spans now include network body consumption and cleanup. The SDK/application still
|
|
418
|
+
owns content decoding, JSON/SSE interpretation and processing errors outside raw-stream I/O.
|
|
419
|
+
Timeouts remain per I/O, not a total stream deadline. Cancellation waits for shielded body
|
|
420
|
+
cleanup before propagating. Close all responses and consumers before shutdown/flush.
|
|
421
|
+
S3 `get` buffers the entire object in memory; use a dedicated streaming adapter for large files.
|
|
379
422
|
|
|
380
423
|
## Development
|
|
381
424
|
|
|
@@ -12,8 +12,8 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
|
|
|
12
12
|
|
|
13
13
|
| Module | Extra | What it gives |
|
|
14
14
|
| --- | --- | --- |
|
|
15
|
-
| `clients_021.http` | none | `HttpClient`:
|
|
16
|
-
| `clients_021.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
15
|
+
| `clients_021.http.httpx` | none | `HttpClient`: buffered requests, streaming and full-body telemetry; native `ObservedTransport` |
|
|
16
|
+
| `clients_021.http.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
17
17
|
| `clients_021.cache.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
18
18
|
| `clients_021.queue.qstash` | none | `QStashClient`: JSON publishing, deduplication, delivery options and forwarded trace context |
|
|
19
19
|
| `clients_021.queue.sqs` | `aws` | `SqsClient`: send, receive, delete, visibility, trace carried in the message |
|
|
@@ -30,10 +30,12 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
30
30
|
|
|
31
31
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
32
32
|
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
33
|
-
contains Redis; `crypto/` contains KMS.
|
|
34
|
-
explicitly:
|
|
33
|
+
contains Redis; `crypto/` contains KMS. `http/` contains the httpx client and httpx2
|
|
34
|
+
transport. Import providers explicitly:
|
|
35
35
|
|
|
36
36
|
```python
|
|
37
|
+
from clients_021.http import HttpClient
|
|
38
|
+
from clients_021.http.httpx2 import ObservedTransport # requires the httpx2 extra
|
|
37
39
|
from clients_021.queue.sqs import SqsClient
|
|
38
40
|
from clients_021.queue.qstash import QStashClient
|
|
39
41
|
from clients_021.storage.s3 import S3Client
|
|
@@ -49,7 +51,9 @@ does not load the optional AWS SDK.
|
|
|
49
51
|
|
|
50
52
|
The 0.1.0 imports `clients_021.redis`, `clients_021.s3` and `clients_021.kms` remain
|
|
51
53
|
compatible aliases for the same classes and result types. Importing a capability package
|
|
52
|
-
does not load its optional SDKs.
|
|
54
|
+
does not load its optional SDKs. `clients_021.HttpClient` and `clients_021.http` keep their
|
|
55
|
+
published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
|
|
56
|
+
`clients_021.http.httpx2.ObservedTransport`.
|
|
53
57
|
|
|
54
58
|
## What every call gets
|
|
55
59
|
|
|
@@ -61,7 +65,8 @@ does not load its optional SDKs.
|
|
|
61
65
|
`crypto.client.operation.*` (KMS), `http.client.request.*` (HTTP, and the HTTP request
|
|
62
66
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
63
67
|
- Redis, SQS, QStash, S3, Vercel Blob, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
64
|
-
with dependency, operation and error type only. HTTP
|
|
68
|
+
with dependency, operation and error type only. HTTP/httpx2 also log failed attempts,
|
|
69
|
+
including body failures and cancellation; QStash/Resend suppress the nested HTTP log.
|
|
65
70
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
66
71
|
refused the connection, throttled or failed with a 5xx. Map it to a 503. Errors that mean
|
|
67
72
|
the request is wrong (access denied, a missing bucket, a wrong Redis type) keep their
|
|
@@ -254,21 +259,58 @@ ciphertext keep their botocore type (they are bugs or attacks, not outages); a t
|
|
|
254
259
|
envelope raises `cryptography`'s `InvalidTag`. The envelope layout is in
|
|
255
260
|
[CONTRACT.md](CONTRACT.md).
|
|
256
261
|
|
|
262
|
+
### HTTP bodies and streaming
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from clients_021.http import HttpClient
|
|
266
|
+
|
|
267
|
+
http = HttpClient(obs, timeout_seconds=10)
|
|
268
|
+
try:
|
|
269
|
+
async with http.stream("GET", download_url, dependency="documents") as response:
|
|
270
|
+
response.raise_for_status()
|
|
271
|
+
async for chunk in response.aiter_bytes():
|
|
272
|
+
await write_chunk(chunk)
|
|
273
|
+
finally:
|
|
274
|
+
await http.aclose()
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
These async clients run on asyncio. `request()` returns a buffered `HttpResponse`; `stream()` yields a native httpx response
|
|
278
|
+
without buffering and accepts an async iterable as upload `content`. Streaming sends once,
|
|
279
|
+
even for GET. The buffered client's configured idempotent retries are unchanged. Close the
|
|
280
|
+
response context before closing the client. For SDKs using native httpx, inject
|
|
281
|
+
`clients_021.http.httpx.ObservedTransport`; httpx2 SDKs use the transport below.
|
|
282
|
+
|
|
283
|
+
Each HTTP attempt emits one span, counter and duration after body EOF and connection close,
|
|
284
|
+
or early close. This includes upload, download, pauses between chunks and cleanup. Body
|
|
285
|
+
errors after 200 headers are recorded as errors. `http.response.body.complete` distinguishes
|
|
286
|
+
EOF from partial/unread close; it does not mean the status or cleanup succeeded. Intentional
|
|
287
|
+
partial close is INFO `dependency.stream_closed`; exceptions, cancellation and HTTP >= 400
|
|
288
|
+
produce WARNING `dependency.call_failed`. No success log, URL, header, body or provider
|
|
289
|
+
error message is emitted. `log_failures=False` disables attempt logs when an enclosing
|
|
290
|
+
operation owns the warning, as QStash and Resend do.
|
|
291
|
+
|
|
292
|
+
These duration semantics and the completion label change the 0.2.x telemetry contract;
|
|
293
|
+
see [migration notes](CONTRACT.md#migration-from-02x) before updating dashboards or alerts.
|
|
294
|
+
|
|
257
295
|
### SDKs on httpx2
|
|
258
296
|
|
|
259
297
|
For an SDK on httpx2:
|
|
260
298
|
|
|
261
299
|
```python
|
|
262
300
|
from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient
|
|
263
|
-
from clients_021.httpx2 import ObservedTransport
|
|
301
|
+
from clients_021.http.httpx2 import ObservedTransport
|
|
264
302
|
|
|
265
303
|
transport = ObservedTransport(obs, dependency="anthropic")
|
|
266
304
|
claude = AsyncAnthropic(http_client=DefaultAsyncHttpxClient(transport=transport))
|
|
267
305
|
```
|
|
268
306
|
|
|
269
|
-
It does not
|
|
307
|
+
It does not inject trace context into third-party API requests unless `propagate=True`.
|
|
270
308
|
The transport preserves SDK exceptions so the SDK retains control of retries and error
|
|
271
|
-
handling; it does not convert them to `DependencyUnavailable`.
|
|
309
|
+
handling; it does not convert them to `DependencyUnavailable`. Its span stays open through
|
|
310
|
+
incremental response reads and close. Use the SDK's stream context manager, or close manual
|
|
311
|
+
responses explicitly; abandoning a stream leaves both its connection and span open. Caller
|
|
312
|
+
processing keeps its own trace context, including when streams are interleaved or consumed
|
|
313
|
+
in another task.
|
|
272
314
|
|
|
273
315
|
## Service integration
|
|
274
316
|
|
|
@@ -302,7 +344,7 @@ async with AsyncExitStack() as resources:
|
|
|
302
344
|
|
|
303
345
|
Retry policies are explicit:
|
|
304
346
|
|
|
305
|
-
- HTTP retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
347
|
+
- Buffered HTTP (`request`) retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
306
348
|
remote protocol/proxy connection failures and 502/503/504. `retries=2` means at most
|
|
307
349
|
three attempts.
|
|
308
350
|
POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
|
|
@@ -336,10 +378,11 @@ stop an SDK request already running in its thread. Timeouts are per SDK/network
|
|
|
336
378
|
not an overall deadline. Limit consumer concurrency and let in-flight work finish before
|
|
337
379
|
closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
|
|
338
380
|
|
|
339
|
-
HTTP
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
381
|
+
HTTP spans now include network body consumption and cleanup. The SDK/application still
|
|
382
|
+
owns content decoding, JSON/SSE interpretation and processing errors outside raw-stream I/O.
|
|
383
|
+
Timeouts remain per I/O, not a total stream deadline. Cancellation waits for shielded body
|
|
384
|
+
cleanup before propagating. Close all responses and consumers before shutdown/flush.
|
|
385
|
+
S3 `get` buffers the entire object in memory; use a dedicated streaming adapter for large files.
|
|
343
386
|
|
|
344
387
|
## Development
|
|
345
388
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "021-python-clients"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email and KMS, built on 021-python-observability"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`clients_021.http`, `clients_021.queue.qstash` and `clients_021.email` (Resend) need no extras;
|
|
4
4
|
`clients_021.cache.redis` needs the `redis` extra; `clients_021.queue.sqs`,
|
|
5
5
|
`clients_021.storage.s3`, `clients_021.email.ses` and `clients_021.crypto.kms` the `aws` extra;
|
|
6
|
-
`clients_021.httpx2` the `httpx2` extra.
|
|
6
|
+
`clients_021.http.httpx2` the `httpx2` extra.
|
|
7
7
|
`clients_021.storage.vercel_blob` needs the `vercel-blob` extra.
|
|
8
8
|
"""
|
|
9
9
|
|
|
@@ -98,7 +98,11 @@ class ResendSender:
|
|
|
98
98
|
}
|
|
99
99
|
inner = transport if transport is not None else httpx.AsyncHTTPTransport()
|
|
100
100
|
self._http = HttpClient(
|
|
101
|
-
obs,
|
|
101
|
+
obs,
|
|
102
|
+
timeout_seconds=timeout_seconds,
|
|
103
|
+
retries=0,
|
|
104
|
+
log_failures=False,
|
|
105
|
+
transport=_WithoutTraceHeaders(inner),
|
|
102
106
|
)
|
|
103
107
|
|
|
104
108
|
async def send(self, message: EmailMessage, *, idempotency_key: str | None = None) -> str:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""HTTP clients and transports; optional implementations are imported explicitly."""
|
|
2
|
+
|
|
3
|
+
from clients_021.http.httpx import (
|
|
4
|
+
IDEMPOTENT_METHODS,
|
|
5
|
+
RETRY_STATUS,
|
|
6
|
+
HttpClient,
|
|
7
|
+
HttpMethod,
|
|
8
|
+
HttpResponse,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = ["IDEMPOTENT_METHODS", "RETRY_STATUS", "HttpClient", "HttpMethod", "HttpResponse"]
|