021-python-clients 0.1.0__tar.gz → 0.2.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.1.0 → 021_python_clients-0.2.0}/CHANGELOG.md +26 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/CONTRACT.md +38 -10
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/CONTRIBUTING.md +13 -8
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/PKG-INFO +81 -13
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/README.md +76 -11
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/pyproject.toml +6 -2
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/__init__.py +4 -3
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/_observe.py +1 -1
- 021_python_clients-0.2.0/src/clients_021/cache/__init__.py +1 -0
- {021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/cache}/redis.py +1 -1
- 021_python_clients-0.2.0/src/clients_021/crypto/__init__.py +1 -0
- {021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/crypto}/kms.py +1 -1
- 021_python_clients-0.2.0/src/clients_021/kms.py +5 -0
- 021_python_clients-0.2.0/src/clients_021/redis.py +5 -0
- 021_python_clients-0.2.0/src/clients_021/s3.py +5 -0
- 021_python_clients-0.2.0/src/clients_021/storage/__init__.py +1 -0
- {021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/storage}/s3.py +1 -1
- 021_python_clients-0.2.0/src/clients_021/storage/vercel_blob.py +169 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_contract.py +3 -3
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_kms.py +1 -1
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_redis.py +1 -1
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_s3.py +1 -1
- 021_python_clients-0.2.0/tests/test_vercel_blob.py +347 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/wheel_minimal.py +12 -1
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/wheel_smoke.py +14 -3
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/.gitignore +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/LICENSE +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/_aws.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/email/__init__.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/email/_message.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/email/resend.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/email/ses.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/errors.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/http.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/httpx2.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/py.typed +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/queue/__init__.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/queue/qstash.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/src/clients_021/queue/sqs.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/__init__.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/conftest.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/qstash_local.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_email.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_http.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_httpx2.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_qstash.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_ses.py +0 -0
- {021_python_clients-0.1.0 → 021_python_clients-0.2.0}/tests/test_sqs.py +0 -0
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are recorded here by release-please from Conventional Commits.
|
|
4
4
|
|
|
5
|
+
## [0.2.0](https://github.com/from021-io/python-clients/compare/v0.1.0...v0.2.0) (2026-10-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **storage:** add observed Vercel Blob client ([9850d03](https://github.com/from021-io/python-clients/commit/9850d0381eed264c5d2962dec1340eb110621480))
|
|
11
|
+
* **storage:** add observed Vercel Blob client ([2cd982d](https://github.com/from021-io/python-clients/commit/2cd982d79b5ebe7e3c5f473c41403803f2803a72))
|
|
12
|
+
* **storage:** add observed Vercel Blob client ([#7](https://github.com/from021-io/python-clients/issues/7)) ([9850d03](https://github.com/from021-io/python-clients/commit/9850d0381eed264c5d2962dec1340eb110621480))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Refactors
|
|
16
|
+
|
|
17
|
+
* **clients:** group providers by capability ([56d4d89](https://github.com/from021-io/python-clients/commit/56d4d89ac69f757a059fed5d19a636a16ce417ed))
|
|
18
|
+
* **clients:** group providers by capability ([3bac4c1](https://github.com/from021-io/python-clients/commit/3bac4c14cf1c9b7157e1dec42c6a827d0fc46890))
|
|
19
|
+
* **clients:** group providers by capability ([#6](https://github.com/from021-io/python-clients/issues/6)) ([56d4d89](https://github.com/from021-io/python-clients/commit/56d4d89ac69f757a059fed5d19a636a16ce417ed))
|
|
20
|
+
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Add an optional Vercel Blob client with observed CRUD, explicit public/private access, bounded operations and SDK-native metadata/errors.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Group Redis, S3 and KMS under cache, storage and crypto, retaining the 0.1.0 imports as compatibility aliases.
|
|
30
|
+
|
|
5
31
|
## 0.1.0 (2026-10-21)
|
|
6
32
|
|
|
7
33
|
|
|
@@ -8,13 +8,19 @@ semantics below is a breaking change.
|
|
|
8
8
|
Queue providers live in `clients_021.queue.sqs` (`SqsClient`, `ReceivedMessage`) and
|
|
9
9
|
`clients_021.queue.qstash` (`QStashClient`, `PublishedMessage`, `QStashResponseError`).
|
|
10
10
|
Use these imports in place of the initial draft's root-level `clients_021.sqs` and
|
|
11
|
-
`clients_021.qstash` paths.
|
|
11
|
+
`clients_021.qstash` paths. These capability paths shipped in 0.1.0.
|
|
12
12
|
Importing `clients_021.queue` or its QStash module does not require the `aws` extra.
|
|
13
13
|
|
|
14
14
|
The queue package groups providers by purpose. SQS owns polling, receipts and visibility;
|
|
15
15
|
QStash owns HTTP delivery. Their public methods and observability contracts remain distinct.
|
|
16
16
|
Shared email types and the interchangeable `EmailSender` protocol live in `clients_021.email`.
|
|
17
17
|
|
|
18
|
+
Redis lives in `clients_021.cache.redis`, S3 in `clients_021.storage.s3` and KMS in
|
|
19
|
+
`clients_021.crypto.kms`. The 0.1.0 root modules (`redis`, `s3`, `kms`) continue to re-export
|
|
20
|
+
the same clients and result types (`ObjectInfo`, `DataKey`). Existing call signatures,
|
|
21
|
+
metric/span names and error semantics are unchanged. Capability package imports do not
|
|
22
|
+
load optional SDKs. HTTP transports remain in `clients_021.http` and `clients_021.httpx2`.
|
|
23
|
+
|
|
18
24
|
## Metrics
|
|
19
25
|
|
|
20
26
|
All on the service's `Observability` meter, so they carry its resource (service name,
|
|
@@ -26,8 +32,8 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
26
32
|
| `db.client.operation.duration` | Histogram / `s` | same | Redis |
|
|
27
33
|
| `messaging.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | SQS, QStash, email (Resend, SES) |
|
|
28
34
|
| `messaging.client.operation.duration` | Histogram / `s` | same | SQS, QStash, email (Resend, SES) |
|
|
29
|
-
| `storage.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | S3 |
|
|
30
|
-
| `storage.client.operation.duration` | Histogram / `s` | same | S3 |
|
|
35
|
+
| `storage.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | S3, Vercel Blob |
|
|
36
|
+
| `storage.client.operation.duration` | Histogram / `s` | same | S3, Vercel Blob |
|
|
31
37
|
| `crypto.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | KMS |
|
|
32
38
|
| `crypto.client.operation.duration` | Histogram / `s` | same | KMS |
|
|
33
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) |
|
|
@@ -36,10 +42,11 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
36
42
|
- `status`: `ok` or `error`. `error.type` only on errors.
|
|
37
43
|
- `operation`: a fixed set per client. Redis: `get_json`, `set_json`, `take_json`,
|
|
38
44
|
`delete`, `incr_window`, `ping`. SQS: `send`, `receive`, `delete`, `change_visibility`.
|
|
39
|
-
S3: `put`, `get`, `head`, `delete`. QStash: `publish`.
|
|
45
|
+
S3 and Vercel Blob: `put`, `get`, `head`, `delete`. QStash: `publish`.
|
|
46
|
+
Email (Resend, SES): `send`. KMS:
|
|
40
47
|
`encrypt`, `decrypt`, `generate_data_key`; `seal` and `unseal` record the KMS call they make
|
|
41
48
|
(`generate_data_key`, `decrypt`), not the local AES work.
|
|
42
|
-
- Redis/SQS/S3/QStash/email/KMS `error.type`: `timeout`, `connection_error`, `throttled`,
|
|
49
|
+
- Redis/SQS/S3/Vercel Blob/QStash/email/KMS `error.type`: `timeout`, `connection_error`, `throttled`,
|
|
43
50
|
`status_<5xx>`, `cancelled`, or the exception class name for errors that are not outages
|
|
44
51
|
(`EmailRejected` for every email rejection; botocore's class, such as `ClientError` or
|
|
45
52
|
`NotFoundException`, for KMS).
|
|
@@ -50,7 +57,7 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
50
57
|
`DependencyUnavailable.reason` describes the failure returned to the caller.
|
|
51
58
|
- httpx2 uses `timeout`, `connection_error`, `cancelled`, or the exception class name.
|
|
52
59
|
HTTP status errors set the span's ERROR status and status-code label, with no `error.type`.
|
|
53
|
-
- A missing S3 object or Redis key is `status=ok`.
|
|
60
|
+
- A missing S3/Blob object or Redis key is `status=ok`.
|
|
54
61
|
- `dependency` is a static identifier, validated consistently with 021-python-observability.
|
|
55
62
|
User ids, tenant ids and request-derived values are forbidden even if they match its syntax.
|
|
56
63
|
|
|
@@ -59,8 +66,8 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
59
66
|
CLIENT spans named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
60
67
|
metric labels plus `db.system.name=redis`, `messaging.system=aws_sqs`,
|
|
61
68
|
`messaging.system=upstash_qstash`, `messaging.system=resend`, `messaging.system=aws_ses`, or
|
|
62
|
-
`rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS
|
|
63
|
-
span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
|
|
69
|
+
`rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS`, or `rpc.system=vercel-blob`.
|
|
70
|
+
Resend's `HTTP <dependency>` span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
|
|
64
71
|
status ≥ 400).
|
|
65
72
|
Cancellation propagates unchanged and records ERROR with `error.type=cancelled`.
|
|
66
73
|
HTTP transports measure through response headers; they do not measure body consumption.
|
|
@@ -78,7 +85,7 @@ Local input validation occurs before either operation is recorded.
|
|
|
78
85
|
When enabled, both caller-supplied trace headers are replaced with the current context.
|
|
79
86
|
- Resend: nothing is sent. The `traceparent`, `tracestate` and `x-request-id` headers that
|
|
80
87
|
021-python-observability's transport adds are removed before the request leaves.
|
|
81
|
-
- SES and
|
|
88
|
+
- SES, KMS and Vercel Blob: nothing is sent by the default transports.
|
|
82
89
|
- SQS: `traceparent` and `tracestate` are message attributes; `ReceivedMessage.trace_context()`
|
|
83
90
|
returns the producer's context. Caller-supplied values for both trace attributes are
|
|
84
91
|
replaced with the current context.
|
|
@@ -127,6 +134,16 @@ tampered ciphertext (`InvalidCiphertextException`) and a ciphertext from another
|
|
|
127
134
|
Every call requires a non-empty encryption context. `unseal` raises `ValueError` for a
|
|
128
135
|
malformed envelope and `cryptography.exceptions.InvalidTag` when the payload was altered.
|
|
129
136
|
|
|
137
|
+
Vercel Blob (`clients_021.storage.vercel_blob`): HTTP 429 maps to `throttled`; 5xx to
|
|
138
|
+
`status_<code>`; HTTP timeouts and total deadlines to `timeout`; network/remote protocol
|
|
139
|
+
failures to `connection_error`. Known transport causes wrapped by the SDK are classified
|
|
140
|
+
without inspecting provider messages. Other errors retain their SDK type, including
|
|
141
|
+
`BlobAccessError`, `BlobStoreNotFoundError`, `BlobPreconditionFailedError`, and
|
|
142
|
+
`BlobStreamError` for malformed acknowledgements. Only `BlobNotFoundError` becomes `None`
|
|
143
|
+
for `get`/`head`, counted as success. `put` returns the SDK's `PutResult` (exported as
|
|
144
|
+
`BlobUpload`); `head` returns its `HeadResult` (`BlobInfo`). These types are provider-specific.
|
|
145
|
+
SDK validation and credential failures can produce an error span but perform no network I/O.
|
|
146
|
+
|
|
130
147
|
## Sealed envelope format
|
|
131
148
|
|
|
132
149
|
`KmsClient.seal` output is stored data, so its layout is part of this contract: version
|
|
@@ -138,7 +155,7 @@ A new data key is generated for every envelope.
|
|
|
138
155
|
|
|
139
156
|
## Lifecycle and retry limits
|
|
140
157
|
|
|
141
|
-
HTTP, Redis, SQS, QStash, S3, Resend, SES and KMS expose `aclose()`. Close them after
|
|
158
|
+
HTTP, Redis, SQS, QStash, S3, Vercel Blob, Resend, SES and KMS expose `aclose()`. Close them after
|
|
142
159
|
in-flight calls complete; the wrapper owns the underlying client, including one supplied
|
|
143
160
|
through the constructor.
|
|
144
161
|
AWS factories configure three total attempts; injected clients retain their own policy.
|
|
@@ -155,9 +172,20 @@ publish again.
|
|
|
155
172
|
`HttpClient.request` accepts at most one of `json`, `form` or raw byte `content`. Passing
|
|
156
173
|
more than one non-None body format raises `ValueError` before sending a request.
|
|
157
174
|
|
|
175
|
+
Vercel Blob uses the optional `vercel-blob>=0.5,<0.6` SDK and explicit read-write credentials,
|
|
176
|
+
store ID and access mode. It owns a shared transport; each operation borrows it in a separate
|
|
177
|
+
SDK session. No environment credentials or global SDK client perform requests. The total
|
|
178
|
+
operation deadline defaults to 10 seconds and includes buffered downloads. Timeout and
|
|
179
|
+
cancellation do not imply a remote write was undone. Neither this wrapper nor the SDK
|
|
180
|
+
retries operations; an injected transport must respect this policy. Overwrite and random
|
|
181
|
+
suffixes default to false. Callers decide whether a repeated write is safe. A closed client
|
|
182
|
+
cannot reopen its pool. No additional HTTP metrics are emitted by this client.
|
|
183
|
+
|
|
158
184
|
## Never recorded
|
|
159
185
|
|
|
160
186
|
Keys, URLs, query strings, bodies, message ids, receipts, object names, bucket or queue
|
|
161
187
|
names, provider error messages. Email: sender, recipient and reply-to addresses, subjects,
|
|
162
188
|
text and HTML bodies, tags, idempotency keys, API keys, configuration set names. KMS: key
|
|
163
189
|
ids, ARNs and aliases, encryption contexts, plaintext, ciphertext and data keys.
|
|
190
|
+
|
|
191
|
+
Vercel Blob: read-write tokens, store IDs, pathnames, delivery URLs, content and ETags.
|
|
@@ -7,25 +7,30 @@ not need to be reimplemented here.
|
|
|
7
7
|
|
|
8
8
|
## Layout and public API
|
|
9
9
|
|
|
10
|
-
Group
|
|
11
|
-
|
|
12
|
-
|
|
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`.
|
|
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
18
|
http.py
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
httpx2.py
|
|
20
|
+
cache/ # redis.py
|
|
21
|
+
storage/ # s3.py, vercel_blob.py
|
|
22
|
+
crypto/ # kms.py
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
The original `clients_021.redis`, `clients_021.s3` and `clients_021.kms` imports remain
|
|
26
|
+
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.
|
|
28
|
+
|
|
24
29
|
Public result and exception types live beside their provider unless they are truly shared.
|
|
25
30
|
A common protocol requires interchangeable operations and semantics: `EmailSender.send`
|
|
26
31
|
qualifies. SQS polling/receipts and QStash HTTP delivery do not share a consumer API, so
|
|
27
|
-
`queue/` groups them without inventing one.
|
|
28
|
-
the capability package must not load optional SDKs.
|
|
32
|
+
`queue/` groups them without inventing one. S3 and Blob also keep provider-specific metadata
|
|
33
|
+
and write options. Provider imports are explicit, and importing the capability package must not load optional SDKs.
|
|
29
34
|
|
|
30
35
|
Use private helpers for actual shared behavior: `_observe.py` owns instrumentation and
|
|
31
36
|
`_aws.py` owns AWS configuration and error classification. Avoid a universal base class:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 021-python-clients
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Observed clients for Redis, SQS, QStash, S3, HTTP, email and KMS, built on 021-python-observability
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
7
7
|
Project-URL: Contract, https://github.com/from021-io/python-clients/blob/main/CONTRACT.md
|
|
@@ -29,6 +29,9 @@ Provides-Extra: httpx2
|
|
|
29
29
|
Requires-Dist: httpx2<3,>=2; extra == 'httpx2'
|
|
30
30
|
Provides-Extra: redis
|
|
31
31
|
Requires-Dist: redis<9,>=6; extra == 'redis'
|
|
32
|
+
Provides-Extra: vercel-blob
|
|
33
|
+
Requires-Dist: httpx2<3,>=2; extra == 'vercel-blob'
|
|
34
|
+
Requires-Dist: vercel-blob<0.6,>=0.5; extra == 'vercel-blob'
|
|
32
35
|
Description-Content-Type: text/markdown
|
|
33
36
|
|
|
34
37
|
# 021-python-clients
|
|
@@ -40,20 +43,21 @@ dependency call gets the same span, the same metrics under a stable dependency n
|
|
|
40
43
|
the same failure type.
|
|
41
44
|
|
|
42
45
|
```bash
|
|
43
|
-
uv add "021-python-clients[redis,aws,httpx2]" # pick the extras you use
|
|
46
|
+
uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras you use
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
| Module | Extra | What it gives |
|
|
47
50
|
| --- | --- | --- |
|
|
48
51
|
| `clients_021.http` | none | `HttpClient`: one httpx client per dependency, timeouts, retries on idempotent methods |
|
|
49
52
|
| `clients_021.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
50
|
-
| `clients_021.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
53
|
+
| `clients_021.cache.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
51
54
|
| `clients_021.queue.qstash` | none | `QStashClient`: JSON publishing, deduplication, delivery options and forwarded trace context |
|
|
52
55
|
| `clients_021.queue.sqs` | `aws` | `SqsClient`: send, receive, delete, visibility, trace carried in the message |
|
|
53
|
-
| `clients_021.s3` | `aws` | `S3Client`: put, get, head, delete, presigned URLs |
|
|
56
|
+
| `clients_021.storage.s3` | `aws` | `S3Client`: put, get, head, delete, presigned URLs |
|
|
57
|
+
| `clients_021.storage.vercel_blob` | `vercel-blob` | `VercelBlobClient`: put, get, head and conditional delete for public/private stores |
|
|
54
58
|
| `clients_021.email` | none | `EmailMessage`, the `EmailSender` protocol, `ResendSender` (Resend's HTTP API) |
|
|
55
59
|
| `clients_021.email.ses` | `aws` | `SesSender`: the same protocol over Amazon SES v2 |
|
|
56
|
-
| `clients_021.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
|
|
60
|
+
| `clients_021.crypto.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
|
|
57
61
|
|
|
58
62
|
Every client takes the service's `Observability` (from `setup_observability`) and a
|
|
59
63
|
`dependency` name, which becomes the `dependency` label of its metrics.
|
|
@@ -61,11 +65,16 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
61
65
|
## Organization
|
|
62
66
|
|
|
63
67
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
64
|
-
Resend and SES; `queue/` contains SQS and QStash
|
|
68
|
+
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
69
|
+
contains Redis; `crypto/` contains KMS. HTTP transports remain at the root. Import providers
|
|
70
|
+
explicitly:
|
|
65
71
|
|
|
66
72
|
```python
|
|
67
73
|
from clients_021.queue.sqs import SqsClient
|
|
68
74
|
from clients_021.queue.qstash import QStashClient
|
|
75
|
+
from clients_021.storage.s3 import S3Client
|
|
76
|
+
from clients_021.cache.redis import RedisClient
|
|
77
|
+
from clients_021.crypto.kms import KmsClient
|
|
69
78
|
```
|
|
70
79
|
|
|
71
80
|
SQS supports polling, receipts and visibility changes; QStash delivers to an HTTP endpoint.
|
|
@@ -74,16 +83,20 @@ because both implement the same send operation. A shared package does not imply
|
|
|
74
83
|
provider implements an interchangeable consumption protocol. Importing `queue` or QStash
|
|
75
84
|
does not load the optional AWS SDK.
|
|
76
85
|
|
|
86
|
+
The 0.1.0 imports `clients_021.redis`, `clients_021.s3` and `clients_021.kms` remain
|
|
87
|
+
compatible aliases for the same classes and result types. Importing a capability package
|
|
88
|
+
does not load its optional SDKs.
|
|
89
|
+
|
|
77
90
|
## What every call gets
|
|
78
91
|
|
|
79
92
|
- A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
80
93
|
system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
|
|
81
|
-
`rpc.service=S3|KMS`).
|
|
94
|
+
`rpc.service=S3|KMS`, `rpc.system=vercel-blob`).
|
|
82
95
|
- A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
|
|
83
|
-
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3),
|
|
96
|
+
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
|
|
84
97
|
`crypto.client.operation.*` (KMS), `http.client.request.*` (HTTP, and the HTTP request
|
|
85
98
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
86
|
-
- Redis, SQS, QStash, S3, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
99
|
+
- Redis, SQS, QStash, S3, Vercel Blob, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
87
100
|
with dependency, operation and error type only. HTTP transports emit spans and metrics.
|
|
88
101
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
89
102
|
refused the connection, throttled or failed with a 5xx. Map it to a 503. Errors that mean
|
|
@@ -100,8 +113,8 @@ them from a tenant, user or request. All clients apply observability's identifie
|
|
|
100
113
|
|
|
101
114
|
```python
|
|
102
115
|
from clients_021 import DependencyUnavailable, HttpClient
|
|
103
|
-
from clients_021.redis import RedisClient
|
|
104
|
-
from clients_021.s3 import S3Client
|
|
116
|
+
from clients_021.cache.redis import RedisClient
|
|
117
|
+
from clients_021.storage.s3 import S3Client
|
|
105
118
|
from clients_021.queue.sqs import SqsClient
|
|
106
119
|
|
|
107
120
|
redis = RedisClient.from_url(obs, settings.redis_url)
|
|
@@ -123,6 +136,59 @@ http = HttpClient(obs, timeout_seconds=10)
|
|
|
123
136
|
response = await http.request("GET", jwks_url, dependency="identity-provider")
|
|
124
137
|
```
|
|
125
138
|
|
|
139
|
+
### Vercel Blob
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from clients_021.storage.vercel_blob import VercelBlobClient
|
|
143
|
+
|
|
144
|
+
files = VercelBlobClient(
|
|
145
|
+
obs,
|
|
146
|
+
token=settings.blob_token,
|
|
147
|
+
store_id=settings.blob_store_id,
|
|
148
|
+
access="private", # Required; must match the store. Public is also supported.
|
|
149
|
+
dependency="documents",
|
|
150
|
+
timeout_seconds=10,
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
uploaded = await files.put("reports/report.pdf", pdf_bytes, content_type="application/pdf")
|
|
154
|
+
content = await files.get(uploaded.url) # bytes, or None for a missing object
|
|
155
|
+
info = await files.head(uploaded.url) # metadata, or None for a missing object
|
|
156
|
+
if info is not None:
|
|
157
|
+
await files.delete(uploaded.url, if_match=info.etag)
|
|
158
|
+
finally:
|
|
159
|
+
await files.aclose()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Uses the [official Python Blob SDK](https://vercel.com/docs/vercel-blob/python-sdk-reference),
|
|
163
|
+
`vercel-blob>=0.5,<0.6`, installed with the `vercel-blob` extra. This is the current functional
|
|
164
|
+
SDK API; it does not depend on the earlier `AsyncBlobClient` API or the full `vercel` bundle.
|
|
165
|
+
Credentials and store access are explicit; no environment or default SDK session is used
|
|
166
|
+
for requests. A read-write token's store must match `store_id` (the `store_` prefix is allowed).
|
|
167
|
+
|
|
168
|
+
`put` returns `BlobUpload` (the SDK's `PutResult`); `head` returns `BlobInfo` (its `HeadResult`).
|
|
169
|
+
These are provider-specific types, not a common S3 protocol. Uploads require a pathname;
|
|
170
|
+
reads, metadata and deletes accept a pathname or a Vercel delivery URL. The SDK validates
|
|
171
|
+
those URLs and does not follow redirects. `get` buffers the entire body in memory.
|
|
172
|
+
|
|
173
|
+
Each operation has one attempt and a finite total deadline, including body consumption.
|
|
174
|
+
The client reuses its connection pool across calls and closes it through `aclose()`,
|
|
175
|
+
including an injected `httpx2.AsyncBaseTransport`. Call `aclose()` only after in-flight calls
|
|
176
|
+
finish. Calls after close fail locally. SDK sessions are scoped to each operation so
|
|
177
|
+
concurrent clients keep their credentials separate. No trace headers are sent by default.
|
|
178
|
+
|
|
179
|
+
`allow_overwrite=False` and `add_random_suffix=False` by default. Both can be explicitly
|
|
180
|
+
enabled, along with `cache_control_max_age`. A timeout may follow a successful upload:
|
|
181
|
+
retrying with a random suffix can create another object; overwriting a stable pathname can
|
|
182
|
+
race with other writers. Deleting a missing object succeeds. `if_match` guards a delete
|
|
183
|
+
against a changed ETag. CDN caches can retain deleted content briefly.
|
|
184
|
+
|
|
185
|
+
429, 5xx, transport failures and deadlines become `DependencyUnavailable`. Authentication,
|
|
186
|
+
missing stores, precondition failures, invalid inputs and malformed responses retain SDK
|
|
187
|
+
exceptions; only a missing object becomes `None` for `get`/`head`. Cancellation propagates.
|
|
188
|
+
The storage operation includes the complete request and response body; this client does
|
|
189
|
+
not emit an additional HTTP metric. Signed URLs, multipart and streaming APIs stay outside
|
|
190
|
+
this initial client.
|
|
191
|
+
|
|
126
192
|
### QStash
|
|
127
193
|
|
|
128
194
|
```python
|
|
@@ -207,7 +273,7 @@ The encryption context is required on every call, so a ciphertext only decrypts
|
|
|
207
273
|
purpose it was made for:
|
|
208
274
|
|
|
209
275
|
```python
|
|
210
|
-
from clients_021.kms import KmsClient
|
|
276
|
+
from clients_021.crypto.kms import KmsClient
|
|
211
277
|
|
|
212
278
|
keys = KmsClient.create(obs, settings.session_key_arn, region="us-east-1")
|
|
213
279
|
context = {"purpose": "refresh-token", "tenant": tenant_id}
|
|
@@ -292,6 +358,8 @@ Retry policies are explicit:
|
|
|
292
358
|
`DependencyUnavailable`; other rejected statuses and invalid acknowledgements raise
|
|
293
359
|
`QStashResponseError` with a status code and no provider body. Delivery timeouts and
|
|
294
360
|
delays use integer seconds and remain subject to the provider plan's limits.
|
|
361
|
+
- Vercel Blob sends each operation once, with a finite total deadline (10 seconds by default).
|
|
362
|
+
An injected transport must also avoid automatic retries of uncertain writes.
|
|
295
363
|
- httpx2 SDKs own their retries; `ObservedTransport` adds no extra retry loop.
|
|
296
364
|
|
|
297
365
|
`DependencyUnavailable` signals a transient dependency failure, **not permission to replay
|
|
@@ -7,20 +7,21 @@ dependency call gets the same span, the same metrics under a stable dependency n
|
|
|
7
7
|
the same failure type.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
uv add "021-python-clients[redis,aws,httpx2]" # pick the extras you use
|
|
10
|
+
uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras you use
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
| Module | Extra | What it gives |
|
|
14
14
|
| --- | --- | --- |
|
|
15
15
|
| `clients_021.http` | none | `HttpClient`: one httpx client per dependency, timeouts, retries on idempotent methods |
|
|
16
16
|
| `clients_021.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
17
|
-
| `clients_021.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
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 |
|
|
20
|
-
| `clients_021.s3` | `aws` | `S3Client`: put, get, head, delete, presigned URLs |
|
|
20
|
+
| `clients_021.storage.s3` | `aws` | `S3Client`: put, get, head, delete, presigned URLs |
|
|
21
|
+
| `clients_021.storage.vercel_blob` | `vercel-blob` | `VercelBlobClient`: put, get, head and conditional delete for public/private stores |
|
|
21
22
|
| `clients_021.email` | none | `EmailMessage`, the `EmailSender` protocol, `ResendSender` (Resend's HTTP API) |
|
|
22
23
|
| `clients_021.email.ses` | `aws` | `SesSender`: the same protocol over Amazon SES v2 |
|
|
23
|
-
| `clients_021.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
|
|
24
|
+
| `clients_021.crypto.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
|
|
24
25
|
|
|
25
26
|
Every client takes the service's `Observability` (from `setup_observability`) and a
|
|
26
27
|
`dependency` name, which becomes the `dependency` label of its metrics.
|
|
@@ -28,11 +29,16 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
28
29
|
## Organization
|
|
29
30
|
|
|
30
31
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
31
|
-
Resend and SES; `queue/` contains SQS and QStash
|
|
32
|
+
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
33
|
+
contains Redis; `crypto/` contains KMS. HTTP transports remain at the root. Import providers
|
|
34
|
+
explicitly:
|
|
32
35
|
|
|
33
36
|
```python
|
|
34
37
|
from clients_021.queue.sqs import SqsClient
|
|
35
38
|
from clients_021.queue.qstash import QStashClient
|
|
39
|
+
from clients_021.storage.s3 import S3Client
|
|
40
|
+
from clients_021.cache.redis import RedisClient
|
|
41
|
+
from clients_021.crypto.kms import KmsClient
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
SQS supports polling, receipts and visibility changes; QStash delivers to an HTTP endpoint.
|
|
@@ -41,16 +47,20 @@ because both implement the same send operation. A shared package does not imply
|
|
|
41
47
|
provider implements an interchangeable consumption protocol. Importing `queue` or QStash
|
|
42
48
|
does not load the optional AWS SDK.
|
|
43
49
|
|
|
50
|
+
The 0.1.0 imports `clients_021.redis`, `clients_021.s3` and `clients_021.kms` remain
|
|
51
|
+
compatible aliases for the same classes and result types. Importing a capability package
|
|
52
|
+
does not load its optional SDKs.
|
|
53
|
+
|
|
44
54
|
## What every call gets
|
|
45
55
|
|
|
46
56
|
- A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
47
57
|
system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
|
|
48
|
-
`rpc.service=S3|KMS`).
|
|
58
|
+
`rpc.service=S3|KMS`, `rpc.system=vercel-blob`).
|
|
49
59
|
- A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
|
|
50
|
-
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3),
|
|
60
|
+
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
|
|
51
61
|
`crypto.client.operation.*` (KMS), `http.client.request.*` (HTTP, and the HTTP request
|
|
52
62
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
53
|
-
- Redis, SQS, QStash, S3, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
63
|
+
- Redis, SQS, QStash, S3, Vercel Blob, email and KMS emit a `dependency.call_failed` warning on failure,
|
|
54
64
|
with dependency, operation and error type only. HTTP transports emit spans and metrics.
|
|
55
65
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
56
66
|
refused the connection, throttled or failed with a 5xx. Map it to a 503. Errors that mean
|
|
@@ -67,8 +77,8 @@ them from a tenant, user or request. All clients apply observability's identifie
|
|
|
67
77
|
|
|
68
78
|
```python
|
|
69
79
|
from clients_021 import DependencyUnavailable, HttpClient
|
|
70
|
-
from clients_021.redis import RedisClient
|
|
71
|
-
from clients_021.s3 import S3Client
|
|
80
|
+
from clients_021.cache.redis import RedisClient
|
|
81
|
+
from clients_021.storage.s3 import S3Client
|
|
72
82
|
from clients_021.queue.sqs import SqsClient
|
|
73
83
|
|
|
74
84
|
redis = RedisClient.from_url(obs, settings.redis_url)
|
|
@@ -90,6 +100,59 @@ http = HttpClient(obs, timeout_seconds=10)
|
|
|
90
100
|
response = await http.request("GET", jwks_url, dependency="identity-provider")
|
|
91
101
|
```
|
|
92
102
|
|
|
103
|
+
### Vercel Blob
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from clients_021.storage.vercel_blob import VercelBlobClient
|
|
107
|
+
|
|
108
|
+
files = VercelBlobClient(
|
|
109
|
+
obs,
|
|
110
|
+
token=settings.blob_token,
|
|
111
|
+
store_id=settings.blob_store_id,
|
|
112
|
+
access="private", # Required; must match the store. Public is also supported.
|
|
113
|
+
dependency="documents",
|
|
114
|
+
timeout_seconds=10,
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
uploaded = await files.put("reports/report.pdf", pdf_bytes, content_type="application/pdf")
|
|
118
|
+
content = await files.get(uploaded.url) # bytes, or None for a missing object
|
|
119
|
+
info = await files.head(uploaded.url) # metadata, or None for a missing object
|
|
120
|
+
if info is not None:
|
|
121
|
+
await files.delete(uploaded.url, if_match=info.etag)
|
|
122
|
+
finally:
|
|
123
|
+
await files.aclose()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Uses the [official Python Blob SDK](https://vercel.com/docs/vercel-blob/python-sdk-reference),
|
|
127
|
+
`vercel-blob>=0.5,<0.6`, installed with the `vercel-blob` extra. This is the current functional
|
|
128
|
+
SDK API; it does not depend on the earlier `AsyncBlobClient` API or the full `vercel` bundle.
|
|
129
|
+
Credentials and store access are explicit; no environment or default SDK session is used
|
|
130
|
+
for requests. A read-write token's store must match `store_id` (the `store_` prefix is allowed).
|
|
131
|
+
|
|
132
|
+
`put` returns `BlobUpload` (the SDK's `PutResult`); `head` returns `BlobInfo` (its `HeadResult`).
|
|
133
|
+
These are provider-specific types, not a common S3 protocol. Uploads require a pathname;
|
|
134
|
+
reads, metadata and deletes accept a pathname or a Vercel delivery URL. The SDK validates
|
|
135
|
+
those URLs and does not follow redirects. `get` buffers the entire body in memory.
|
|
136
|
+
|
|
137
|
+
Each operation has one attempt and a finite total deadline, including body consumption.
|
|
138
|
+
The client reuses its connection pool across calls and closes it through `aclose()`,
|
|
139
|
+
including an injected `httpx2.AsyncBaseTransport`. Call `aclose()` only after in-flight calls
|
|
140
|
+
finish. Calls after close fail locally. SDK sessions are scoped to each operation so
|
|
141
|
+
concurrent clients keep their credentials separate. No trace headers are sent by default.
|
|
142
|
+
|
|
143
|
+
`allow_overwrite=False` and `add_random_suffix=False` by default. Both can be explicitly
|
|
144
|
+
enabled, along with `cache_control_max_age`. A timeout may follow a successful upload:
|
|
145
|
+
retrying with a random suffix can create another object; overwriting a stable pathname can
|
|
146
|
+
race with other writers. Deleting a missing object succeeds. `if_match` guards a delete
|
|
147
|
+
against a changed ETag. CDN caches can retain deleted content briefly.
|
|
148
|
+
|
|
149
|
+
429, 5xx, transport failures and deadlines become `DependencyUnavailable`. Authentication,
|
|
150
|
+
missing stores, precondition failures, invalid inputs and malformed responses retain SDK
|
|
151
|
+
exceptions; only a missing object becomes `None` for `get`/`head`. Cancellation propagates.
|
|
152
|
+
The storage operation includes the complete request and response body; this client does
|
|
153
|
+
not emit an additional HTTP metric. Signed URLs, multipart and streaming APIs stay outside
|
|
154
|
+
this initial client.
|
|
155
|
+
|
|
93
156
|
### QStash
|
|
94
157
|
|
|
95
158
|
```python
|
|
@@ -174,7 +237,7 @@ The encryption context is required on every call, so a ciphertext only decrypts
|
|
|
174
237
|
purpose it was made for:
|
|
175
238
|
|
|
176
239
|
```python
|
|
177
|
-
from clients_021.kms import KmsClient
|
|
240
|
+
from clients_021.crypto.kms import KmsClient
|
|
178
241
|
|
|
179
242
|
keys = KmsClient.create(obs, settings.session_key_arn, region="us-east-1")
|
|
180
243
|
context = {"purpose": "refresh-token", "tenant": tenant_id}
|
|
@@ -259,6 +322,8 @@ Retry policies are explicit:
|
|
|
259
322
|
`DependencyUnavailable`; other rejected statuses and invalid acknowledgements raise
|
|
260
323
|
`QStashResponseError` with a status code and no provider body. Delivery timeouts and
|
|
261
324
|
delays use integer seconds and remain subject to the provider plan's limits.
|
|
325
|
+
- Vercel Blob sends each operation once, with a finite total deadline (10 seconds by default).
|
|
326
|
+
An injected transport must also avoid automatic retries of uncertain writes.
|
|
262
327
|
- httpx2 SDKs own their retries; `ObservedTransport` adds no extra retry loop.
|
|
263
328
|
|
|
264
329
|
`DependencyUnavailable` signals a transient dependency failure, **not permission to replay
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "021-python-clients"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Observed clients for Redis, SQS, QStash, S3, HTTP, email and KMS, built on 021-python-observability"
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
@@ -34,6 +34,10 @@ redis = ["redis>=6,<9"]
|
|
|
34
34
|
# cryptography is for KmsClient.seal / unseal (AES-256-GCM envelopes).
|
|
35
35
|
aws = ["boto3>=1.40,<2", "cryptography>=44"]
|
|
36
36
|
httpx2 = ["httpx2>=2,<3"]
|
|
37
|
+
vercel-blob = [
|
|
38
|
+
"httpx2>=2,<3",
|
|
39
|
+
"vercel-blob>=0.5,<0.6",
|
|
40
|
+
]
|
|
37
41
|
|
|
38
42
|
[project.urls]
|
|
39
43
|
Repository = "https://github.com/from021-io/python-clients"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Observed clients for the external services Python services call.
|
|
2
2
|
|
|
3
3
|
`clients_021.http`, `clients_021.queue.qstash` and `clients_021.email` (Resend) need no extras;
|
|
4
|
-
`clients_021.redis` needs the `redis` extra; `clients_021.queue.sqs`,
|
|
5
|
-
`clients_021.email.ses` and `clients_021.kms` the `aws` extra;
|
|
6
|
-
`httpx2` extra.
|
|
4
|
+
`clients_021.cache.redis` needs the `redis` extra; `clients_021.queue.sqs`,
|
|
5
|
+
`clients_021.storage.s3`, `clients_021.email.ses` and `clients_021.crypto.kms` the `aws` extra;
|
|
6
|
+
`clients_021.httpx2` the `httpx2` extra.
|
|
7
|
+
`clients_021.storage.vercel_blob` needs the `vercel-blob` extra.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from importlib.metadata import version
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The `db` family reuses the instruments 021-python-observability already creates for SQL
|
|
4
4
|
(`db.client.operation.*`), so Redis and Postgres share dashboards. `messaging` (SQS, email),
|
|
5
|
-
`storage` (S3) and `crypto` (KMS) get their own pair on the same meter. Attributes are
|
|
5
|
+
`storage` (S3, Vercel Blob) and `crypto` (KMS) get their own pair on the same meter. Attributes are
|
|
6
6
|
bounded: dependency, operation, status and error.type; never keys, URLs, bodies or ids.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cache clients. Import providers explicitly; SDKs are optional extras."""
|
{021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/cache}/redis.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Redis, observed: JSON values with a TTL, one-time reads, windowed counters and a ping.
|
|
2
2
|
|
|
3
|
-
from clients_021.redis import RedisClient
|
|
3
|
+
from clients_021.cache.redis import RedisClient
|
|
4
4
|
|
|
5
5
|
redis = RedisClient.from_url(obs, settings.redis_url, dependency="redis")
|
|
6
6
|
await redis.set_json("session:abc", {"user": "ana"}, ttl_seconds=1800)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Crypto clients. Import providers explicitly; SDKs are optional extras."""
|
{021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/crypto}/kms.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AWS KMS, observed: encrypt, decrypt and data keys, always bound to an encryption context.
|
|
2
2
|
|
|
3
|
-
from clients_021.kms import KmsClient
|
|
3
|
+
from clients_021.crypto.kms import KmsClient
|
|
4
4
|
|
|
5
5
|
keys = KmsClient.create(obs, settings.session_key_id, region="us-east-1")
|
|
6
6
|
context = {"purpose": "session", "tenant": tenant_id}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Storage clients. Import providers explicitly; SDKs are optional extras."""
|
{021_python_clients-0.1.0/src/clients_021 → 021_python_clients-0.2.0/src/clients_021/storage}/s3.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""S3 (or any S3-compatible store), observed: put, get, head, delete and presigned URLs.
|
|
2
2
|
|
|
3
|
-
from clients_021.s3 import S3Client
|
|
3
|
+
from clients_021.storage.s3 import S3Client
|
|
4
4
|
|
|
5
5
|
files = S3Client.create(obs, bucket="service-uploads", region="us-east-1")
|
|
6
6
|
await files.put("reports/2026-10.pdf", data, content_type="application/pdf")
|