021-python-clients 0.3.0__tar.gz → 0.5.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.3.0 → 021_python_clients-0.5.0}/CHANGELOG.md +26 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/CONTRACT.md +85 -11
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/CONTRIBUTING.md +6 -1
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/PKG-INFO +142 -12
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/README.md +138 -8
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/pyproject.toml +6 -6
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/__init__.py +5 -2
- 021_python_clients-0.5.0/src/clients_021/_observe.py +193 -0
- 021_python_clients-0.5.0/src/clients_021/compute/__init__.py +1 -0
- 021_python_clients-0.5.0/src/clients_021/compute/kubernetes.py +438 -0
- 021_python_clients-0.5.0/src/clients_021/featureflags/__init__.py +42 -0
- 021_python_clients-0.5.0/src/clients_021/featureflags/appconfig.py +246 -0
- 021_python_clients-0.5.0/src/clients_021/llm/__init__.py +1 -0
- 021_python_clients-0.5.0/src/clients_021/llm/bedrock.py +250 -0
- 021_python_clients-0.5.0/src/clients_021/secrets/__init__.py +1 -0
- 021_python_clients-0.5.0/src/clients_021/secrets/secrets_manager.py +127 -0
- 021_python_clients-0.5.0/tests/test_appconfig_flags.py +222 -0
- 021_python_clients-0.5.0/tests/test_bedrock.py +290 -0
- 021_python_clients-0.5.0/tests/test_kubernetes.py +498 -0
- 021_python_clients-0.5.0/tests/test_secrets_manager.py +258 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/wheel_minimal.py +31 -2
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/wheel_smoke.py +10 -0
- 021_python_clients-0.3.0/src/clients_021/_observe.py +0 -106
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/.gitignore +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/LICENSE +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/_aws.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/cache/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/cache/redis.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/crypto/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/crypto/kms.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/email/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/email/_message.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/email/resend.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/email/ses.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/errors.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/http/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/http/_observe.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/http/httpx.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/http/httpx2.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/httpx2.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/kms.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/py.typed +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/queue/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/queue/qstash.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/queue/sqs.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/redis.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/s3.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/storage/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/storage/s3.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/src/clients_021/storage/vercel_blob.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/__init__.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/conftest.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/qstash_local.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_contract.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_email.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_http.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_http_stream_lifecycle.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_http_streaming.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_httpx2.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_kms.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_qstash.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_redis.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_s3.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_ses.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_sqs.py +0 -0
- {021_python_clients-0.3.0 → 021_python_clients-0.5.0}/tests/test_vercel_blob.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.5.0](https://github.com/from021-io/python-clients/compare/v0.4.0...v0.5.0) (2026-10-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **compute:** create and delete Job-owned Secrets ([7f62939](https://github.com/from021-io/python-clients/commit/7f62939c08b5bc1672543e8819cb0781028320b7))
|
|
11
|
+
* **compute:** create and delete Job-owned Secrets ([9507539](https://github.com/from021-io/python-clients/commit/9507539a84882b29a6f6baa63e1a9df17c08add8))
|
|
12
|
+
* **featureflags:** add AppConfig feature flags client ([039c2b2](https://github.com/from021-io/python-clients/commit/039c2b2cc27f3cc3715a892032e369047bb108a3))
|
|
13
|
+
* **featureflags:** add AppConfig feature flags client ([2c40ddb](https://github.com/from021-io/python-clients/commit/2c40ddbb493dca39d483406b28ffa7cfa883faba))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug fixes
|
|
17
|
+
|
|
18
|
+
* **ci:** actualiza herramientas de instalación y publicación ([5c35e84](https://github.com/from021-io/python-clients/commit/5c35e844f679ce905335629ed7d3b4608b77c988))
|
|
19
|
+
* **ci:** actualiza herramientas de instalación y publicación ([234a84a](https://github.com/from021-io/python-clients/commit/234a84af950a9e0d2f9460e807d124411b367953))
|
|
20
|
+
* **deps:** support observability 0.4 ([#23](https://github.com/from021-io/python-clients/issues/23)) ([7363894](https://github.com/from021-io/python-clients/commit/7363894a8010abd345a60a54f29a55247e3307a3))
|
|
21
|
+
|
|
22
|
+
## [0.4.0](https://github.com/from021-io/python-clients/compare/v0.3.0...v0.4.0) (2026-10-22)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **compute:** add Kubernetes Jobs client over the HTTP client ([9b3c369](https://github.com/from021-io/python-clients/commit/9b3c36930f6a070d3fee9844e86c0d27f871346b))
|
|
28
|
+
* **llm:** add observed Bedrock Runtime client for SDKs ([5216293](https://github.com/from021-io/python-clients/commit/52162939416f58c610f4f4e217d911f5e5efdea9))
|
|
29
|
+
* **secrets:** add observed Secrets Manager client ([f14042f](https://github.com/from021-io/python-clients/commit/f14042f064ca61e8d267b338b033a12fd41690eb))
|
|
30
|
+
|
|
5
31
|
## [0.3.0](https://github.com/from021-io/python-clients/compare/v0.2.0...v0.3.0) (2026-10-21)
|
|
6
32
|
|
|
7
33
|
|
|
@@ -15,8 +15,10 @@ The queue package groups providers by purpose. SQS owns polling, receipts and vi
|
|
|
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
|
|
19
|
-
`clients_021.crypto.kms
|
|
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
|
|
20
22
|
the same clients and result types (`ObjectInfo`, `DataKey`). Existing call signatures,
|
|
21
23
|
metric/span names and error semantics are unchanged. Capability package imports do not
|
|
22
24
|
load optional SDKs. HTTP implementations live in `clients_021.http.httpx` and
|
|
@@ -40,6 +42,15 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
40
42
|
| `storage.client.operation.duration` | Histogram / `s` | same | S3, Vercel Blob |
|
|
41
43
|
| `crypto.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | KMS |
|
|
42
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
|
+
| `config.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Feature flags (AppConfig) |
|
|
53
|
+
| `config.client.operation.duration` | Histogram / `s` | same | Feature flags (AppConfig) |
|
|
43
54
|
| `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) |
|
|
44
55
|
| `http.client.request.duration` | Histogram / `s` | same | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
|
|
45
56
|
|
|
@@ -49,8 +60,19 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
49
60
|
S3 and Vercel Blob: `put`, `get`, `head`, `delete`. QStash: `publish`.
|
|
50
61
|
Email (Resend, SES): `send`. KMS:
|
|
51
62
|
`encrypt`, `decrypt`, `generate_data_key`; `seal` and `unseal` record the KMS call they make
|
|
52
|
-
(`generate_data_key`, `decrypt`), not the local AES work.
|
|
53
|
-
|
|
63
|
+
(`generate_data_key`, `decrypt`), not the local AES work. Secrets Manager: `get`.
|
|
64
|
+
Feature flags: `start_session`, `get_configuration`; reading a flag from the snapshot
|
|
65
|
+
records nothing.
|
|
66
|
+
Bedrock: `converse`, `converse_stream`, `invoke_model`, `invoke_model_with_response_stream`,
|
|
67
|
+
`count_tokens`. A streaming operation ends when its event stream is exhausted, fails or is
|
|
68
|
+
closed, not when the call returns. Kubernetes Jobs: `create`, `get`, `delete`, `create_secret`,
|
|
69
|
+
`delete_secret`; each also records its HTTP request (`http.client.request.*`) under the same
|
|
70
|
+
`dependency`. A missing Job or Secret (`get`, `delete`, `delete_secret`) and an existing one
|
|
71
|
+
(`create`, `create_secret`) are `status=ok`.
|
|
72
|
+
- `token.type`: `input`, `output`, `cache_read`, `cache_write`, as reported by Bedrock in
|
|
73
|
+
the Converse response or the ConverseStream `metadata` event. InvokeModel bodies are
|
|
74
|
+
model-specific and are not read.
|
|
75
|
+
- Redis/SQS/S3/Vercel Blob/QStash/email/KMS/Secrets Manager/Bedrock/Kubernetes Jobs/feature flags `error.type`: `timeout`, `connection_error`, `throttled`,
|
|
54
76
|
`status_<5xx>`, `cancelled`, or the exception class name for errors that are not outages
|
|
55
77
|
(`EmailRejected` for every email rejection; botocore's class, such as `ClientError` or
|
|
56
78
|
`NotFoundException`, for KMS).
|
|
@@ -70,7 +92,11 @@ version, environment). Durations use 021-python-observability's buckets.
|
|
|
70
92
|
CLIENT spans named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
71
93
|
metric labels plus `db.system.name=redis`, `messaging.system=aws_sqs`,
|
|
72
94
|
`messaging.system=upstash_qstash`, `messaging.system=resend`, `messaging.system=aws_ses`, or
|
|
73
|
-
`rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS
|
|
95
|
+
`rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS` / `rpc.service=AppConfigData` / `rpc.service=SecretsManager` /
|
|
96
|
+
`rpc.service=BedrockRuntime` (plus `gen_ai.system=aws.bedrock`), `rpc.system=vercel-blob`, or
|
|
97
|
+
`rpc.system=kubernetes` + `rpc.service=Job` (`rpc.service=Secret` for `create_secret` and
|
|
98
|
+
`delete_secret`). Kubernetes Jobs' `HTTP <dependency>` span is a child of its
|
|
99
|
+
`<dependency> <operation>` span.
|
|
74
100
|
Resend's `HTTP <dependency>` span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
|
|
75
101
|
status ≥ 400).
|
|
76
102
|
Cancellation propagates unchanged and records ERROR with `error.type=cancelled`.
|
|
@@ -97,7 +123,7 @@ Local input validation occurs before either operation is recorded.
|
|
|
97
123
|
|
|
98
124
|
## Logging and coverage limits
|
|
99
125
|
|
|
100
|
-
Redis, SQS, QStash, S3, Vercel Blob, Resend, SES and
|
|
126
|
+
Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock, Kubernetes Jobs and feature flags emit a structured
|
|
101
127
|
`dependency.call_failed` warning when an observed remote operation raises an exception.
|
|
102
128
|
Fields are `dependency`, `operation` and `error_type`; provider messages and exception
|
|
103
129
|
chains are not logged. Successful calls and cancellation do not emit this warning;
|
|
@@ -112,7 +138,7 @@ attempts emit no log. Intentional partial close emits `dependency.stream_closed`
|
|
|
112
138
|
with dependency and operation; it is not an outage.
|
|
113
139
|
|
|
114
140
|
`log_failures=False` suppresses these HTTP attempt logs without disabling metrics or spans.
|
|
115
|
-
QStash and
|
|
141
|
+
QStash, Resend and Kubernetes Jobs set this option because their logical operation already emits a failure
|
|
116
142
|
warning; they do not log the same failure at both levels. SDK retry loops can produce one
|
|
117
143
|
warning per failed attempt. HTTP counters measure attempts, not logical API/SDK calls.
|
|
118
144
|
|
|
@@ -137,7 +163,7 @@ telemetry reaches an external backend.
|
|
|
137
163
|
When enabled, both caller-supplied trace headers are replaced with the current context.
|
|
138
164
|
- Resend: nothing is sent. The `traceparent`, `tracestate` and `x-request-id` headers that
|
|
139
165
|
the HTTP transport adds are removed before the request leaves.
|
|
140
|
-
- SES, KMS and Vercel Blob: nothing is sent by the default transports.
|
|
166
|
+
- SES, KMS, Secrets Manager, Bedrock, feature flags and Vercel Blob: nothing is sent by the default transports.
|
|
141
167
|
- SQS: `traceparent` and `tracestate` are message attributes; `ReceivedMessage.trace_context()`
|
|
142
168
|
returns the producer's context. Caller-supplied values for both trace attributes are
|
|
143
169
|
replaced with the current context.
|
|
@@ -180,6 +206,14 @@ it. Resend's request is sent once; `idempotency_key` becomes its `Idempotency-Ke
|
|
|
180
206
|
(1–256 characters, kept by Resend for 24 hours). SES has no idempotency key: the argument is
|
|
181
207
|
validated and ignored.
|
|
182
208
|
|
|
209
|
+
Feature flags: throttling, 5xx, timeouts and connection failures are `DependencyUnavailable`,
|
|
210
|
+
raised only while no snapshot exists; once one does, a failed refresh serves it stale and
|
|
211
|
+
retries after `retry_seconds`. Access denied and missing application, environment or profile
|
|
212
|
+
keep their botocore type. A configuration that is not feature flag JSON (an object of
|
|
213
|
+
`{"enabled": bool, ...}` entries) raises `ValueError` on the first load and is ignored, with a
|
|
214
|
+
`featureflags.invalid_configuration` warning, afterwards. An expired session token
|
|
215
|
+
(`BadRequestException`) opens one new session.
|
|
216
|
+
|
|
183
217
|
KMS: throttling, 5xx (`KMSInternalException`, `DependencyTimeoutException`,
|
|
184
218
|
`KeyUnavailableException`), timeouts and connection failures are `DependencyUnavailable`.
|
|
185
219
|
Access denied, disabled or missing keys, invalid key state, a wrong encryption context or a
|
|
@@ -188,6 +222,39 @@ tampered ciphertext (`InvalidCiphertextException`) and a ciphertext from another
|
|
|
188
222
|
Every call requires a non-empty encryption context. `unseal` raises `ValueError` for a
|
|
189
223
|
malformed envelope and `cryptography.exceptions.InvalidTag` when the payload was altered.
|
|
190
224
|
|
|
225
|
+
Secrets Manager: throttling, 5xx (`InternalServiceError`), timeouts and connection failures
|
|
226
|
+
are `DependencyUnavailable`. A missing secret (`ResourceNotFoundException`), access denied,
|
|
227
|
+
a value KMS cannot decrypt (`DecryptionFailure`), a secret scheduled for deletion
|
|
228
|
+
(`InvalidRequestException`) and invalid parameters keep their botocore type. `get` is a read:
|
|
229
|
+
retrying it is safe. A version with neither `SecretString` nor `SecretBinary` raises
|
|
230
|
+
`ValueError`.
|
|
231
|
+
|
|
232
|
+
Bedrock: throttling (`ThrottlingException`, and `throttlingException` inside an event stream),
|
|
233
|
+
5xx, `ModelNotReadyException` (`throttled`), the stream errors `serviceUnavailableException`,
|
|
234
|
+
`internalServerException` and `modelStreamErrorException`, timeouts and connection failures
|
|
235
|
+
are `DependencyUnavailable`. Access denied, validation errors, a missing model or profile and
|
|
236
|
+
quota limits (`ServiceQuotaExceededException`) keep their botocore type. Leaving a stream
|
|
237
|
+
early or calling its `close()` ends the operation as `ok` and logs `dependency.stream_closed`
|
|
238
|
+
at INFO. A model call is not idempotent in cost: botocore's retries bill each attempt.
|
|
239
|
+
|
|
240
|
+
Kubernetes Jobs: 429 is `throttled`, 5xx `status_<5xx>`, transport timeouts and connection
|
|
241
|
+
failures `timeout` / `connection_error`, all `DependencyUnavailable`. A 409 whose reason is
|
|
242
|
+
`AlreadyExists` on `create` or `create_secret` and a 404 on `get`, `delete` or `delete_secret`
|
|
243
|
+
are results, not errors. Any other
|
|
244
|
+
non-success status raises `KubernetesApiError(status_code, reason)`, where `reason` is the
|
|
245
|
+
Status object's machine-readable reason (`Forbidden`, `Invalid`, `Conflict`, ...) or
|
|
246
|
+
`Unknown`; a 201 whose name does not match raises it with `InvalidResponse`. POST is sent
|
|
247
|
+
once; GET and DELETE retry 502/503/504 and network failures once. Local validation (DNS-label
|
|
248
|
+
names and namespace, `metadata.name` required, `generateName` refused, `kind` must be Job)
|
|
249
|
+
happens before any request. `JobCreated.uid` (on 201) and `JobStatus.uid` carry the Job's
|
|
250
|
+
`metadata.uid`, or `None` when absent or when `create` found an existing Job.
|
|
251
|
+
`create_secret(name, data, *, job, job_uid)` posts an `Opaque`, `immutable` Secret with base64
|
|
252
|
+
`data` and one owner reference (`batch/v1` Job, name and uid, no `blockOwnerDeletion`); an
|
|
253
|
+
existing Secret is kept unchanged (`SecretCreated(created=False)`). Its local validation
|
|
254
|
+
(DNS-label Secret and Job names, non-empty `data`, keys of 1-253 letters, digits, `-`, `_`
|
|
255
|
+
or `.`, `str` or `bytes` values, a uid of letters, digits and `-`) raises `ValueError`
|
|
256
|
+
before any request, with a message that names neither keys nor values.
|
|
257
|
+
|
|
191
258
|
Vercel Blob (`clients_021.storage.vercel_blob`): HTTP 429 maps to `throttled`; 5xx to
|
|
192
259
|
`status_<code>`; HTTP timeouts and total deadlines to `timeout`; network/remote protocol
|
|
193
260
|
failures to `connection_error`. Known transport causes wrapped by the SDK are classified
|
|
@@ -209,7 +276,8 @@ A new data key is generated for every envelope.
|
|
|
209
276
|
|
|
210
277
|
## Lifecycle and retry limits
|
|
211
278
|
|
|
212
|
-
HTTP, Redis, SQS, QStash, S3, Vercel Blob, Resend, SES
|
|
279
|
+
HTTP, Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock, Kubernetes
|
|
280
|
+
Jobs and feature flags expose `aclose()` (Bedrock also `close()`). Close them after
|
|
213
281
|
in-flight calls complete; the wrapper owns the underlying client, including one supplied
|
|
214
282
|
through the constructor.
|
|
215
283
|
AWS factories configure three total attempts; injected clients retain their own policy.
|
|
@@ -261,9 +329,15 @@ these clients, or each request would be counted twice.
|
|
|
261
329
|
|
|
262
330
|
## Never recorded
|
|
263
331
|
|
|
264
|
-
|
|
332
|
+
Feature flags: application, environment and profile identifiers, flag names, attributes, values
|
|
333
|
+
and session tokens. Keys, URLs, query strings, bodies, message ids, receipts, object names, bucket or queue
|
|
265
334
|
names, provider error messages. Email: sender, recipient and reply-to addresses, subjects,
|
|
266
335
|
text and HTML bodies, tags, idempotency keys, API keys, configuration set names. KMS: key
|
|
267
|
-
ids, ARNs and aliases, encryption contexts, plaintext, ciphertext and data keys.
|
|
336
|
+
ids, ARNs and aliases, encryption contexts, plaintext, ciphertext and data keys. Secrets
|
|
337
|
+
Manager: secret ids, names and ARNs, version ids, version stages and secret values. Bedrock:
|
|
338
|
+
model ids, inference profile ARNs, prompts, system prompts, outputs, tool arguments and
|
|
339
|
+
results, request ids and guardrail details. Kubernetes Jobs: namespaces, Job names, labels,
|
|
340
|
+
manifests, API server addresses, service account tokens and Status messages; Secret names,
|
|
341
|
+
data keys and values (plain or base64), owner Job names and uids.
|
|
268
342
|
|
|
269
343
|
Vercel Blob: read-write tokens, store IDs, pathnames, delivery URLs, content and ETags.
|
|
@@ -19,6 +19,9 @@ clients_021/
|
|
|
19
19
|
cache/ # redis.py
|
|
20
20
|
storage/ # s3.py, vercel_blob.py
|
|
21
21
|
crypto/ # kms.py
|
|
22
|
+
secrets/ # secrets_manager.py
|
|
23
|
+
llm/ # bedrock.py
|
|
24
|
+
compute/ # kubernetes.py (over the shared HTTP client, no extra)
|
|
22
25
|
```
|
|
23
26
|
|
|
24
27
|
The original `clients_021.redis`, `clients_021.s3` and `clients_021.kms` imports remain
|
|
@@ -62,7 +65,9 @@ Each implementation must make these decisions explicit in README.md and CONTRACT
|
|
|
62
65
|
| Telemetry | Choose a metric family, fixed operations and system attribute. Never record URLs, credentials, bodies, object names or request-derived ids. |
|
|
63
66
|
| Propagation | State the default and opt-out. Replace `traceparent` and `tracestate` together, including removing stale state. |
|
|
64
67
|
|
|
65
|
-
Use `_observe.observed` for a logical cache, messaging, storage or crypto operation
|
|
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
|
|
66
71
|
clients may also emit the transport's HTTP span/metrics: one logical operation and one
|
|
67
72
|
network request are different measurements. Do not sum those counters as distinct
|
|
68
73
|
business operations. Disable nested HTTP attempt logs with `log_failures=False` when the
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 021-python-clients
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email and
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs and AppConfig feature flags, 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
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Keywords: email,httpx,kms,observability,opentelemetry,qstash,redis,resend,s3,ses,sqs
|
|
10
|
+
Keywords: appconfig,bedrock,email,feature-flags,httpx,kms,kubernetes,observability,opentelemetry,qstash,redis,resend,s3,secrets-manager,ses,sqs
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Topic :: System :: Monitoring
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.12
|
|
22
|
-
Requires-Dist: 021-python-observability<0.
|
|
22
|
+
Requires-Dist: 021-python-observability<0.5,>=0.3
|
|
23
23
|
Requires-Dist: httpx<1,>=0.28
|
|
24
24
|
Requires-Dist: opentelemetry-api<2,>=1.44
|
|
25
25
|
Provides-Extra: aws
|
|
@@ -58,6 +58,11 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
|
|
|
58
58
|
| `clients_021.email` | none | `EmailMessage`, the `EmailSender` protocol, `ResendSender` (Resend's HTTP API) |
|
|
59
59
|
| `clients_021.email.ses` | `aws` | `SesSender`: the same protocol over Amazon SES v2 |
|
|
60
60
|
| `clients_021.crypto.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
|
|
61
|
+
| `clients_021.secrets.secrets_manager` | `aws` | `SecretsManagerClient`: read a secret by id, version id or stage; the value is never printed |
|
|
62
|
+
| `clients_021.compute.kubernetes` | none | `KubernetesJobsClient`: create (idempotent by name), read and delete Jobs in a namespace, and the Secrets they own; in-cluster service account |
|
|
63
|
+
| `clients_021.featureflags` | none | `Flag` and the `FeatureFlags` protocol services type against |
|
|
64
|
+
| `clients_021.featureflags.appconfig` | `aws` | `AppConfigFlags`: AWS AppConfig feature flags, cached and refreshed on an interval |
|
|
65
|
+
| `clients_021.llm.bedrock` | `aws` | `BedrockRuntime`: an observed `bedrock-runtime` client to pass to SDKs such as LangChain's `ChatBedrockConverse`; streams measured to the end, token usage counted |
|
|
61
66
|
|
|
62
67
|
Every client takes the service's `Observability` (from `setup_observability`) and a
|
|
63
68
|
`dependency` name, which becomes the `dependency` label of its metrics.
|
|
@@ -66,7 +71,7 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
66
71
|
|
|
67
72
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
68
73
|
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
69
|
-
contains Redis; `crypto/` contains KMS. `http/` contains the httpx client and httpx2
|
|
74
|
+
contains Redis; `crypto/` contains KMS; `secrets/` contains Secrets Manager; `llm/` contains Bedrock; `compute/` contains Kubernetes Jobs; `featureflags/` contains AppConfig. `http/` contains the httpx client and httpx2
|
|
70
75
|
transport. Import providers explicitly:
|
|
71
76
|
|
|
72
77
|
```python
|
|
@@ -77,6 +82,9 @@ from clients_021.queue.qstash import QStashClient
|
|
|
77
82
|
from clients_021.storage.s3 import S3Client
|
|
78
83
|
from clients_021.cache.redis import RedisClient
|
|
79
84
|
from clients_021.crypto.kms import KmsClient
|
|
85
|
+
from clients_021.secrets.secrets_manager import SecretsManagerClient
|
|
86
|
+
from clients_021.llm.bedrock import BedrockRuntime
|
|
87
|
+
from clients_021.compute.kubernetes import KubernetesJobsClient
|
|
80
88
|
```
|
|
81
89
|
|
|
82
90
|
SQS supports polling, receipts and visibility changes; QStash delivers to an HTTP endpoint.
|
|
@@ -95,12 +103,16 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
|
|
|
95
103
|
|
|
96
104
|
- A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
97
105
|
system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
|
|
98
|
-
`rpc.service=S3|KMS`, `rpc.system=vercel-blob`
|
|
106
|
+
`rpc.service=S3|KMS|SecretsManager|BedrockRuntime|AppConfigData`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
|
|
107
|
+
Bedrock also `gen_ai.system=aws.bedrock`).
|
|
99
108
|
- A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
|
|
100
109
|
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
|
|
101
|
-
`crypto.client.operation.*` (KMS), `
|
|
110
|
+
`crypto.client.operation.*` (KMS), `secrets.client.operation.*` (Secrets Manager),
|
|
111
|
+
`llm.client.operation.*` and `llm.client.token.usage` (Bedrock), `compute.client.operation.*`
|
|
112
|
+
(Kubernetes Jobs), `config.client.operation.*` (feature flags),
|
|
113
|
+
`http.client.request.*` (HTTP, and the HTTP request
|
|
102
114
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
103
|
-
- Redis, SQS, QStash, S3, Vercel Blob, email and
|
|
115
|
+
- Redis, SQS, QStash, S3, Vercel Blob, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a `dependency.call_failed` warning on failure,
|
|
104
116
|
with dependency, operation and error type only. HTTP/httpx2 also log failed attempts,
|
|
105
117
|
including body failures and cancellation; QStash/Resend suppress the nested HTTP log.
|
|
106
118
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
@@ -109,7 +121,9 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
|
|
|
109
121
|
original type: they are bugs, not outages.
|
|
110
122
|
|
|
111
123
|
Never in this library's spans, metrics or logs: keys, URLs, bodies, message ids or object
|
|
112
|
-
names; email addresses, subjects, bodies or tags;
|
|
124
|
+
names; email addresses, subjects, bodies or tags; secret ids, version ids, stages and
|
|
125
|
+
values; model ids, prompts and outputs; namespaces, Job names, manifests and service
|
|
126
|
+
account tokens; feature flag names, values and AppConfig identifiers; KMS key ids, encryption contexts,
|
|
113
127
|
plaintext, ciphertext or data keys. Use static dependency names such as `identity-provider` or `uploads`; never derive
|
|
114
128
|
them from a tenant, user or request. All clients apply observability's identifier rule
|
|
115
129
|
(1–96 letters, digits, `_`, `.` or `-`, starting with a letter or digit).
|
|
@@ -272,6 +286,118 @@ except EmailRejected as rejected: # the address, the domain or the credentials;
|
|
|
272
286
|
SES has no idempotency key (the argument is ignored): deduplicate in the caller when a retry
|
|
273
287
|
must not send twice. Tags are for provider analytics; never put personal data in them.
|
|
274
288
|
|
|
289
|
+
### Kubernetes Jobs
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from clients_021.compute.kubernetes import KubernetesJobsClient
|
|
293
|
+
|
|
294
|
+
jobs = KubernetesJobsClient.in_cluster(obs, namespace="agent-runs")
|
|
295
|
+
created = await jobs.create({"metadata": {"name": "cmd-4821"}, "spec": job_spec})
|
|
296
|
+
status = await jobs.get("cmd-4821") # JobStatus(phase="running", ...) or None
|
|
297
|
+
await jobs.delete("cmd-4821", propagation="Background")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
No SDK: it calls the `batch/v1` REST API through `HttpClient`. `in_cluster` uses the pod's
|
|
301
|
+
service account (CA bundle, namespace, and a token it re-reads at most once a minute because
|
|
302
|
+
projected tokens rotate). `create` is idempotent by name: `generateName` is refused, and an
|
|
303
|
+
existing Job returns `created=False`, so a caller can repeat it after a timeout. `get`
|
|
304
|
+
returns `None` and `delete` returns `False` for a missing Job. POST is sent once; GET and
|
|
305
|
+
DELETE retry 502, 503, 504 and network failures once. 429, 5xx, timeouts and connection
|
|
306
|
+
failures raise `DependencyUnavailable`; other rejections raise `KubernetesApiError` with the
|
|
307
|
+
status code and the API's reason, never its message.
|
|
308
|
+
|
|
309
|
+
A value the pod needs but that must not be readable in the Job's spec (a run token, say) goes
|
|
310
|
+
in a Secret the Job owns:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
created = await jobs.create(manifest) # the container reads env from secretKeyRef
|
|
314
|
+
uid = created.uid or (await jobs.get(name)).uid # None in created when the Job existed
|
|
315
|
+
await jobs.create_secret(f"{name}-token", {"token": run_token}, job=name, job_uid=uid)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```yaml
|
|
319
|
+
env:
|
|
320
|
+
- name: RUN_TOKEN
|
|
321
|
+
valueFrom: { secretKeyRef: { name: cmd-4821-token, key: token } }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The Secret is `Opaque` and `immutable`, with an owner reference to the Job's name and uid, so
|
|
325
|
+
the garbage collector deletes it with the Job (including through `ttlSecondsAfterFinished`).
|
|
326
|
+
The Job is created first because the reference needs its uid; the kubelet keeps retrying a
|
|
327
|
+
pod whose Secret does not exist yet. `create_secret` is idempotent by name like `create`: a
|
|
328
|
+
409 `AlreadyExists` returns `created=False` and keeps the existing Secret without comparing or
|
|
329
|
+
updating it, so a retry must reuse the same value (or `delete_secret` and create again).
|
|
330
|
+
`delete_secret` returns `False` when there was none. `blockOwnerDeletion` is not set, so the
|
|
331
|
+
caller needs only `create` and `delete` on `secrets` (no `get`, and nothing on
|
|
332
|
+
`jobs/finalizers`). Secret names, data keys and values never reach telemetry, logs or error
|
|
333
|
+
messages; the request body is still in the chained httpx exception's `request`, so do not log
|
|
334
|
+
`__cause__` details.
|
|
335
|
+
|
|
336
|
+
### Bedrock
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from clients_021.llm.bedrock import BedrockRuntime
|
|
340
|
+
from langchain_aws import ChatBedrockConverse
|
|
341
|
+
|
|
342
|
+
runtime = BedrockRuntime.create(obs, region="us-east-1")
|
|
343
|
+
model = ChatBedrockConverse(model=settings.inference_profile, client=runtime)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`BedrockRuntime` keeps boto3's call signatures for `converse`, `converse_stream`,
|
|
347
|
+
`invoke_model`, `invoke_model_with_response_stream` and `count_tokens`, and passes every
|
|
348
|
+
other attribute (`meta`, `exceptions`) through to the boto3 client, so an SDK can take it
|
|
349
|
+
where it expects one. Calls stay synchronous like boto3's. A streaming call is measured
|
|
350
|
+
until its event stream ends; leaving the loop early or calling `close()` ends it as `ok`.
|
|
351
|
+
Converse and ConverseStream add the reported input, output and cache tokens to
|
|
352
|
+
`llm.client.token.usage`. Throttling (also mid-stream), 5xx, a model still loading and the
|
|
353
|
+
stream errors Bedrock documents as retryable raise `DependencyUnavailable`; access denied,
|
|
354
|
+
validation errors, a missing model and quota limits keep their botocore type. A retried
|
|
355
|
+
model call is billed again.
|
|
356
|
+
|
|
357
|
+
### Secrets Manager
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from clients_021.secrets.secrets_manager import SecretsManagerClient
|
|
361
|
+
|
|
362
|
+
secrets = SecretsManagerClient.create(obs, region="us-east-1")
|
|
363
|
+
secret = await secrets.get("connections/brain")
|
|
364
|
+
token = secret.text() # SecretString; secret.binary() for SecretBinary
|
|
365
|
+
previous = await secrets.get("connections/brain", version_stage="AWSPREVIOUS")
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`get` reads the current version unless a `version_id` or a `version_stage` is given (not
|
|
369
|
+
both). The client does not cache: every call is a billed request, so keep a short-lived
|
|
370
|
+
copy when the same secret is read often. A missing secret, access denied, a value KMS cannot
|
|
371
|
+
decrypt or a secret scheduled for deletion keep their botocore type; throttling, 5xx and
|
|
372
|
+
timeouts raise `DependencyUnavailable`. `repr(secret)` never shows the value.
|
|
373
|
+
|
|
374
|
+
### Feature flags (AWS AppConfig)
|
|
375
|
+
|
|
376
|
+
Reads [AppConfig feature flags](https://aws.amazon.com/blogs/mt/using-aws-appconfig-feature-flags/)
|
|
377
|
+
through the AppConfig Data API and keeps a snapshot in memory; evaluating a flag is a lookup:
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
from clients_021.featureflags import FeatureFlags # the protocol, no extra needed
|
|
381
|
+
from clients_021.featureflags.appconfig import AppConfigFlags
|
|
382
|
+
|
|
383
|
+
flags: FeatureFlags = AppConfigFlags.create(
|
|
384
|
+
obs, settings.appconfig_app, settings.appconfig_env, settings.appconfig_profile,
|
|
385
|
+
region="us-east-1",
|
|
386
|
+
)
|
|
387
|
+
if await flags.is_enabled("new-checkout"): # unknown flag -> default=False
|
|
388
|
+
...
|
|
389
|
+
limit = await flags.flag("rate-limit") # Flag(enabled=True, attributes={"rps": 50})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The snapshot is refreshed by the first request after `refresh_seconds` (default 45, minimum
|
|
393
|
+
15), one poll at a time. If a refresh fails while a snapshot exists, the stale one keeps
|
|
394
|
+
serving and the next attempt is 15 seconds later; with no snapshot yet the outage raises
|
|
395
|
+
`DependencyUnavailable`. A configuration that is not valid flag JSON raises `ValueError` on
|
|
396
|
+
the first load and is ignored with a warning afterwards. Supports plain flags and their
|
|
397
|
+
attributes; multi-variant flags and rules need the AppConfig Agent, which evaluates them.
|
|
398
|
+
The IAM role needs `appconfig:StartConfigurationSession` and
|
|
399
|
+
`appconfig:GetLatestConfiguration` on the configuration profile.
|
|
400
|
+
|
|
275
401
|
### KMS
|
|
276
402
|
|
|
277
403
|
The encryption context is required on every call, so a ciphertext only decrypts for the
|
|
@@ -375,6 +501,8 @@ async with AsyncExitStack() as resources:
|
|
|
375
501
|
resources.push_async_callback(email.aclose)
|
|
376
502
|
keys = KmsClient.create(obs, settings.session_key_arn, region="us-east-1")
|
|
377
503
|
resources.push_async_callback(keys.aclose)
|
|
504
|
+
secrets = SecretsManagerClient.create(obs, region="us-east-1")
|
|
505
|
+
resources.push_async_callback(secrets.aclose)
|
|
378
506
|
# Run the service here. Stop its background workers before leaving this scope.
|
|
379
507
|
```
|
|
380
508
|
|
|
@@ -386,7 +514,7 @@ Retry policies are explicit:
|
|
|
386
514
|
POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
|
|
387
515
|
their original exception. HTTP status responses are returned for the caller to handle.
|
|
388
516
|
- Resend sends each request once (POST); send an `idempotency_key` and retry in the caller.
|
|
389
|
-
- AWS factories (SQS, S3, SES, KMS) use botocore standard retries with at most **three total
|
|
517
|
+
- AWS factories (SQS, S3, SES, KMS, Secrets Manager, Bedrock, feature flags) use botocore standard retries with at most **three total
|
|
390
518
|
attempts**.
|
|
391
519
|
Injected SDK clients keep their configured policy. SQS consumers must tolerate duplicate
|
|
392
520
|
messages; a timeout does not prove that a send or write was not applied.
|
|
@@ -409,7 +537,7 @@ any operation**. Map it at the service boundary and apply the operation's idempo
|
|
|
409
537
|
Its message and structured fields are safe to log when dependency names are static. Its
|
|
410
538
|
chained SDK exception may contain sensitive provider data; do not log exception chains.
|
|
411
539
|
|
|
412
|
-
S3, SQS, SES and
|
|
540
|
+
S3, SQS, SES, KMS, Secrets Manager and feature flags run the synchronous AWS SDK in worker threads. Cancelling the await does not
|
|
413
541
|
stop an SDK request already running in its thread. Timeouts are per SDK/network attempt,
|
|
414
542
|
not an overall deadline. Limit consumer concurrency and let in-flight work finish before
|
|
415
543
|
closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
|
|
@@ -433,6 +561,8 @@ deduplication, start the [official local server](https://upstash.com/docs/qstash
|
|
|
433
561
|
set its `QSTASH_URL` and `QSTASH_TOKEN`, and run `uv run python tests/qstash_local.py`.
|
|
434
562
|
That smoke test accepts only a loopback QStash server and binds its destination dynamically.
|
|
435
563
|
|
|
436
|
-
|
|
564
|
+
Kubernetes Jobs tests use httpx's `MockTransport`. Bedrock tests use botocore's Stubber and
|
|
565
|
+
fake event streams. SQS, S3, SES, KMS and Secrets
|
|
566
|
+
Manager tests run against [moto](https://github.com/getmoto/moto) (plus
|
|
437
567
|
botocore's Stubber where moto ignores a field or differs from AWS); Resend tests use httpx's
|
|
438
568
|
`MockTransport`; Redis tests need a real Redis. Licensed under Apache-2.0.
|