021-python-clients 0.4.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.
Files changed (65) hide show
  1. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/CHANGELOG.md +17 -0
  2. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/CONTRACT.md +39 -15
  3. 021_python_clients-0.4.0/README.md → 021_python_clients-0.5.0/PKG-INFO +99 -7
  4. 021_python_clients-0.4.0/PKG-INFO → 021_python_clients-0.5.0/README.md +63 -43
  5. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/pyproject.toml +5 -5
  6. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/__init__.py +2 -1
  7. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/_observe.py +7 -6
  8. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/compute/kubernetes.py +131 -7
  9. 021_python_clients-0.5.0/src/clients_021/featureflags/__init__.py +42 -0
  10. 021_python_clients-0.5.0/src/clients_021/featureflags/appconfig.py +246 -0
  11. 021_python_clients-0.5.0/tests/test_appconfig_flags.py +222 -0
  12. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_kubernetes.py +199 -0
  13. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/wheel_minimal.py +6 -2
  14. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/wheel_smoke.py +3 -0
  15. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/.gitignore +0 -0
  16. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/CONTRIBUTING.md +0 -0
  17. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/LICENSE +0 -0
  18. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/_aws.py +0 -0
  19. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/cache/__init__.py +0 -0
  20. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/cache/redis.py +0 -0
  21. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/compute/__init__.py +0 -0
  22. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/crypto/__init__.py +0 -0
  23. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/crypto/kms.py +0 -0
  24. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/email/__init__.py +0 -0
  25. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/email/_message.py +0 -0
  26. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/email/resend.py +0 -0
  27. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/email/ses.py +0 -0
  28. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/errors.py +0 -0
  29. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/http/__init__.py +0 -0
  30. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/http/_observe.py +0 -0
  31. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/http/httpx.py +0 -0
  32. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/http/httpx2.py +0 -0
  33. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/httpx2.py +0 -0
  34. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/kms.py +0 -0
  35. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/llm/__init__.py +0 -0
  36. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/llm/bedrock.py +0 -0
  37. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/py.typed +0 -0
  38. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/queue/__init__.py +0 -0
  39. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/queue/qstash.py +0 -0
  40. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/queue/sqs.py +0 -0
  41. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/redis.py +0 -0
  42. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/s3.py +0 -0
  43. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/secrets/__init__.py +0 -0
  44. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/secrets/secrets_manager.py +0 -0
  45. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/storage/__init__.py +0 -0
  46. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/storage/s3.py +0 -0
  47. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/src/clients_021/storage/vercel_blob.py +0 -0
  48. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/__init__.py +0 -0
  49. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/conftest.py +0 -0
  50. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/qstash_local.py +0 -0
  51. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_bedrock.py +0 -0
  52. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_contract.py +0 -0
  53. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_email.py +0 -0
  54. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_http.py +0 -0
  55. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_http_stream_lifecycle.py +0 -0
  56. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_http_streaming.py +0 -0
  57. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_httpx2.py +0 -0
  58. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_kms.py +0 -0
  59. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_qstash.py +0 -0
  60. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_redis.py +0 -0
  61. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_s3.py +0 -0
  62. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_secrets_manager.py +0 -0
  63. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_ses.py +0 -0
  64. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_sqs.py +0 -0
  65. {021_python_clients-0.4.0 → 021_python_clients-0.5.0}/tests/test_vercel_blob.py +0 -0
@@ -2,6 +2,23 @@
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
+
5
22
  ## [0.4.0](https://github.com/from021-io/python-clients/compare/v0.3.0...v0.4.0) (2026-10-22)
6
23
 
7
24
 
@@ -49,6 +49,8 @@ version, environment). Durations use 021-python-observability's buckets.
49
49
  | `llm.client.token.usage` | Counter / `{token}` | dependency, operation, token.type | Bedrock (Converse and ConverseStream) |
50
50
  | `compute.client.operation.count` | Counter / `{operation}` | dependency, operation, status, error.type | Kubernetes Jobs |
51
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) |
52
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) |
53
55
  | `http.client.request.duration` | Histogram / `s` | same | HTTP, httpx2, QStash transport, Resend (its HTTP request) |
54
56
 
@@ -59,15 +61,18 @@ version, environment). Durations use 021-python-observability's buckets.
59
61
  Email (Resend, SES): `send`. KMS:
60
62
  `encrypt`, `decrypt`, `generate_data_key`; `seal` and `unseal` record the KMS call they make
61
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.
62
66
  Bedrock: `converse`, `converse_stream`, `invoke_model`, `invoke_model_with_response_stream`,
63
67
  `count_tokens`. A streaming operation ends when its event stream is exhausted, fails or is
64
- closed, not when the call returns. Kubernetes Jobs: `create`, `get`, `delete`; each also
65
- records its HTTP request (`http.client.request.*`) under the same `dependency`. A missing
66
- Job (`get`, `delete`) and an existing one (`create`) are `status=ok`.
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`.
67
72
  - `token.type`: `input`, `output`, `cache_read`, `cache_write`, as reported by Bedrock in
68
73
  the Converse response or the ConverseStream `metadata` event. InvokeModel bodies are
69
74
  model-specific and are not read.
70
- - Redis/SQS/S3/Vercel Blob/QStash/email/KMS/Secrets Manager/Bedrock/Kubernetes Jobs `error.type`: `timeout`, `connection_error`, `throttled`,
75
+ - Redis/SQS/S3/Vercel Blob/QStash/email/KMS/Secrets Manager/Bedrock/Kubernetes Jobs/feature flags `error.type`: `timeout`, `connection_error`, `throttled`,
71
76
  `status_<5xx>`, `cancelled`, or the exception class name for errors that are not outages
72
77
  (`EmailRejected` for every email rejection; botocore's class, such as `ClientError` or
73
78
  `NotFoundException`, for KMS).
@@ -87,10 +92,11 @@ version, environment). Durations use 021-python-observability's buckets.
87
92
  CLIENT spans named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
88
93
  metric labels plus `db.system.name=redis`, `messaging.system=aws_sqs`,
89
94
  `messaging.system=upstash_qstash`, `messaging.system=resend`, `messaging.system=aws_ses`, or
90
- `rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS` / `rpc.service=SecretsManager` /
95
+ `rpc.system=aws-api` + `rpc.service=S3` / `rpc.service=KMS` / `rpc.service=AppConfigData` / `rpc.service=SecretsManager` /
91
96
  `rpc.service=BedrockRuntime` (plus `gen_ai.system=aws.bedrock`), `rpc.system=vercel-blob`, or
92
- `rpc.system=kubernetes` + `rpc.service=Job`. Kubernetes Jobs' `HTTP <dependency>` span is a
93
- child of its `<dependency> <operation>` span.
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.
94
100
  Resend's `HTTP <dependency>` span is a child of its `<dependency> send` span. Status ERROR on failure (and on HTTP
95
101
  status ≥ 400).
96
102
  Cancellation propagates unchanged and records ERROR with `error.type=cancelled`.
@@ -117,7 +123,7 @@ Local input validation occurs before either operation is recorded.
117
123
 
118
124
  ## Logging and coverage limits
119
125
 
120
- Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a structured
126
+ Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock, Kubernetes Jobs and feature flags emit a structured
121
127
  `dependency.call_failed` warning when an observed remote operation raises an exception.
122
128
  Fields are `dependency`, `operation` and `error_type`; provider messages and exception
123
129
  chains are not logged. Successful calls and cancellation do not emit this warning;
@@ -157,7 +163,7 @@ telemetry reaches an external backend.
157
163
  When enabled, both caller-supplied trace headers are replaced with the current context.
158
164
  - Resend: nothing is sent. The `traceparent`, `tracestate` and `x-request-id` headers that
159
165
  the HTTP transport adds are removed before the request leaves.
160
- - SES, KMS, Secrets Manager, Bedrock and Vercel Blob: nothing is sent by the default transports.
166
+ - SES, KMS, Secrets Manager, Bedrock, feature flags and Vercel Blob: nothing is sent by the default transports.
161
167
  - SQS: `traceparent` and `tracestate` are message attributes; `ReceivedMessage.trace_context()`
162
168
  returns the producer's context. Caller-supplied values for both trace attributes are
163
169
  replaced with the current context.
@@ -200,6 +206,14 @@ it. Resend's request is sent once; `idempotency_key` becomes its `Idempotency-Ke
200
206
  (1–256 characters, kept by Resend for 24 hours). SES has no idempotency key: the argument is
201
207
  validated and ignored.
202
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
+
203
217
  KMS: throttling, 5xx (`KMSInternalException`, `DependencyTimeoutException`,
204
218
  `KeyUnavailableException`), timeouts and connection failures are `DependencyUnavailable`.
205
219
  Access denied, disabled or missing keys, invalid key state, a wrong encryption context or a
@@ -225,13 +239,21 @@ at INFO. A model call is not idempotent in cost: botocore's retries bill each at
225
239
 
226
240
  Kubernetes Jobs: 429 is `throttled`, 5xx `status_<5xx>`, transport timeouts and connection
227
241
  failures `timeout` / `connection_error`, all `DependencyUnavailable`. A 409 whose reason is
228
- `AlreadyExists` on `create` and a 404 on `get` or `delete` are results, not errors. Any other
242
+ `AlreadyExists` on `create` or `create_secret` and a 404 on `get`, `delete` or `delete_secret`
243
+ are results, not errors. Any other
229
244
  non-success status raises `KubernetesApiError(status_code, reason)`, where `reason` is the
230
245
  Status object's machine-readable reason (`Forbidden`, `Invalid`, `Conflict`, ...) or
231
246
  `Unknown`; a 201 whose name does not match raises it with `InvalidResponse`. POST is sent
232
247
  once; GET and DELETE retry 502/503/504 and network failures once. Local validation (DNS-label
233
248
  names and namespace, `metadata.name` required, `generateName` refused, `kind` must be Job)
234
- happens before any request.
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.
235
257
 
236
258
  Vercel Blob (`clients_021.storage.vercel_blob`): HTTP 429 maps to `throttled`; 5xx to
237
259
  `status_<code>`; HTTP timeouts and total deadlines to `timeout`; network/remote protocol
@@ -254,8 +276,8 @@ A new data key is generated for every envelope.
254
276
 
255
277
  ## Lifecycle and retry limits
256
278
 
257
- HTTP, Redis, SQS, QStash, S3, Vercel Blob, Resend, SES, KMS, Secrets Manager, Bedrock and Kubernetes
258
- Jobs expose `aclose()` (Bedrock also `close()`). Close them after
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
259
281
  in-flight calls complete; the wrapper owns the underlying client, including one supplied
260
282
  through the constructor.
261
283
  AWS factories configure three total attempts; injected clients retain their own policy.
@@ -307,13 +329,15 @@ these clients, or each request would be counted twice.
307
329
 
308
330
  ## Never recorded
309
331
 
310
- Keys, URLs, query strings, bodies, message ids, receipts, object names, bucket or queue
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
311
334
  names, provider error messages. Email: sender, recipient and reply-to addresses, subjects,
312
335
  text and HTML bodies, tags, idempotency keys, API keys, configuration set names. KMS: key
313
336
  ids, ARNs and aliases, encryption contexts, plaintext, ciphertext and data keys. Secrets
314
337
  Manager: secret ids, names and ARNs, version ids, version stages and secret values. Bedrock:
315
338
  model ids, inference profile ARNs, prompts, system prompts, outputs, tool arguments and
316
339
  results, request ids and guardrail details. Kubernetes Jobs: namespaces, Job names, labels,
317
- manifests, API server addresses, service account tokens and Status messages.
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.
318
342
 
319
343
  Vercel Blob: read-write tokens, store IDs, pathnames, delivery URLs, content and ETags.
@@ -1,3 +1,39 @@
1
+ Metadata-Version: 2.5
2
+ Name: 021-python-clients
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
+ Project-URL: Repository, https://github.com/from021-io/python-clients
6
+ Project-URL: Changelog, https://github.com/from021-io/python-clients/blob/main/CHANGELOG.md
7
+ Project-URL: Contract, https://github.com/from021-io/python-clients/blob/main/CONTRACT.md
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: appconfig,bedrock,email,feature-flags,httpx,kms,kubernetes,observability,opentelemetry,qstash,redis,resend,s3,secrets-manager,ses,sqs
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: System :: Monitoring
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: 021-python-observability<0.5,>=0.3
23
+ Requires-Dist: httpx<1,>=0.28
24
+ Requires-Dist: opentelemetry-api<2,>=1.44
25
+ Provides-Extra: aws
26
+ Requires-Dist: boto3<2,>=1.40; extra == 'aws'
27
+ Requires-Dist: cryptography>=44; extra == 'aws'
28
+ Provides-Extra: httpx2
29
+ Requires-Dist: httpx2<3,>=2; extra == 'httpx2'
30
+ Provides-Extra: redis
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'
35
+ Description-Content-Type: text/markdown
36
+
1
37
  # 021-python-clients
2
38
 
3
39
  Clients for the external services our Python services call, with the observability of
@@ -23,7 +59,9 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
23
59
  | `clients_021.email.ses` | `aws` | `SesSender`: the same protocol over Amazon SES v2 |
24
60
  | `clients_021.crypto.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
25
61
  | `clients_021.secrets.secrets_manager` | `aws` | `SecretsManagerClient`: read a secret by id, version id or stage; the value is never printed |
26
- | `clients_021.compute.kubernetes` | none | `KubernetesJobsClient`: create (idempotent by name), read and delete Jobs in a namespace, in-cluster service account |
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 |
27
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 |
28
66
 
29
67
  Every client takes the service's `Observability` (from `setup_observability`) and a
@@ -33,7 +71,7 @@ Every client takes the service's `Observability` (from `setup_observability`) an
33
71
 
34
72
  Providers with the same purpose live under one capability package: `email/` contains
35
73
  Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
36
- contains Redis; `crypto/` contains KMS; `secrets/` contains Secrets Manager; `llm/` contains Bedrock; `compute/` contains Kubernetes Jobs. `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
37
75
  transport. Import providers explicitly:
38
76
 
39
77
  ```python
@@ -65,13 +103,13 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
65
103
 
66
104
  - A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
67
105
  system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
68
- `rpc.service=S3|KMS|SecretsManager|BedrockRuntime`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
106
+ `rpc.service=S3|KMS|SecretsManager|BedrockRuntime|AppConfigData`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
69
107
  Bedrock also `gen_ai.system=aws.bedrock`).
70
108
  - A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
71
109
  `messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
72
110
  `crypto.client.operation.*` (KMS), `secrets.client.operation.*` (Secrets Manager),
73
111
  `llm.client.operation.*` and `llm.client.token.usage` (Bedrock), `compute.client.operation.*`
74
- (Kubernetes Jobs),
112
+ (Kubernetes Jobs), `config.client.operation.*` (feature flags),
75
113
  `http.client.request.*` (HTTP, and the HTTP request
76
114
  under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
77
115
  - Redis, SQS, QStash, S3, Vercel Blob, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a `dependency.call_failed` warning on failure,
@@ -85,7 +123,7 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
85
123
  Never in this library's spans, metrics or logs: keys, URLs, bodies, message ids or object
86
124
  names; email addresses, subjects, bodies or tags; secret ids, version ids, stages and
87
125
  values; model ids, prompts and outputs; namespaces, Job names, manifests and service
88
- account tokens; KMS key ids, encryption contexts,
126
+ account tokens; feature flag names, values and AppConfig identifiers; KMS key ids, encryption contexts,
89
127
  plaintext, ciphertext or data keys. Use static dependency names such as `identity-provider` or `uploads`; never derive
90
128
  them from a tenant, user or request. All clients apply observability's identifier rule
91
129
  (1–96 letters, digits, `_`, `.` or `-`, starting with a letter or digit).
@@ -268,6 +306,33 @@ DELETE retry 502, 503, 504 and network failures once. 429, 5xx, timeouts and con
268
306
  failures raise `DependencyUnavailable`; other rejections raise `KubernetesApiError` with the
269
307
  status code and the API's reason, never its message.
270
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
+
271
336
  ### Bedrock
272
337
 
273
338
  ```python
@@ -306,6 +371,33 @@ copy when the same secret is read often. A missing secret, access denied, a valu
306
371
  decrypt or a secret scheduled for deletion keep their botocore type; throttling, 5xx and
307
372
  timeouts raise `DependencyUnavailable`. `repr(secret)` never shows the value.
308
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
+
309
401
  ### KMS
310
402
 
311
403
  The encryption context is required on every call, so a ciphertext only decrypts for the
@@ -422,7 +514,7 @@ Retry policies are explicit:
422
514
  POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
423
515
  their original exception. HTTP status responses are returned for the caller to handle.
424
516
  - Resend sends each request once (POST); send an `idempotency_key` and retry in the caller.
425
- - AWS factories (SQS, S3, SES, KMS, Secrets Manager, Bedrock) 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
426
518
  attempts**.
427
519
  Injected SDK clients keep their configured policy. SQS consumers must tolerate duplicate
428
520
  messages; a timeout does not prove that a send or write was not applied.
@@ -445,7 +537,7 @@ any operation**. Map it at the service boundary and apply the operation's idempo
445
537
  Its message and structured fields are safe to log when dependency names are static. Its
446
538
  chained SDK exception may contain sensitive provider data; do not log exception chains.
447
539
 
448
- S3, SQS, SES, KMS and Secrets Manager run the synchronous AWS SDK in worker threads. Cancelling the await does not
540
+ S3, SQS, SES, KMS, Secrets Manager and feature flags run the synchronous AWS SDK in worker threads. Cancelling the await does not
449
541
  stop an SDK request already running in its thread. Timeouts are per SDK/network attempt,
450
542
  not an overall deadline. Limit consumer concurrency and let in-flight work finish before
451
543
  closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
@@ -1,39 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: 021-python-clients
3
- Version: 0.4.0
4
- Summary: Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs, built on 021-python-observability
5
- Project-URL: Repository, https://github.com/from021-io/python-clients
6
- Project-URL: Changelog, https://github.com/from021-io/python-clients/blob/main/CHANGELOG.md
7
- Project-URL: Contract, https://github.com/from021-io/python-clients/blob/main/CONTRACT.md
8
- License-Expression: Apache-2.0
9
- License-File: LICENSE
10
- Keywords: bedrock,email,httpx,kms,kubernetes,observability,opentelemetry,qstash,redis,resend,s3,secrets-manager,ses,sqs
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Programming Language :: Python :: 3.14
19
- Classifier: Topic :: System :: Monitoring
20
- Classifier: Typing :: Typed
21
- Requires-Python: >=3.12
22
- Requires-Dist: 021-python-observability<0.4,>=0.3
23
- Requires-Dist: httpx<1,>=0.28
24
- Requires-Dist: opentelemetry-api<2,>=1.44
25
- Provides-Extra: aws
26
- Requires-Dist: boto3<2,>=1.40; extra == 'aws'
27
- Requires-Dist: cryptography>=44; extra == 'aws'
28
- Provides-Extra: httpx2
29
- Requires-Dist: httpx2<3,>=2; extra == 'httpx2'
30
- Provides-Extra: redis
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'
35
- Description-Content-Type: text/markdown
36
-
37
1
  # 021-python-clients
38
2
 
39
3
  Clients for the external services our Python services call, with the observability of
@@ -59,7 +23,9 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
59
23
  | `clients_021.email.ses` | `aws` | `SesSender`: the same protocol over Amazon SES v2 |
60
24
  | `clients_021.crypto.kms` | `aws` | `KmsClient`: encrypt, decrypt and data keys bound to an encryption context; AES-256-GCM envelopes |
61
25
  | `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, in-cluster service account |
26
+ | `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 |
27
+ | `clients_021.featureflags` | none | `Flag` and the `FeatureFlags` protocol services type against |
28
+ | `clients_021.featureflags.appconfig` | `aws` | `AppConfigFlags`: AWS AppConfig feature flags, cached and refreshed on an interval |
63
29
  | `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 |
64
30
 
65
31
  Every client takes the service's `Observability` (from `setup_observability`) and a
@@ -69,7 +35,7 @@ Every client takes the service's `Observability` (from `setup_observability`) an
69
35
 
70
36
  Providers with the same purpose live under one capability package: `email/` contains
71
37
  Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
72
- contains Redis; `crypto/` contains KMS; `secrets/` contains Secrets Manager; `llm/` contains Bedrock; `compute/` contains Kubernetes Jobs. `http/` contains the httpx client and httpx2
38
+ 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
73
39
  transport. Import providers explicitly:
74
40
 
75
41
  ```python
@@ -101,13 +67,13 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
101
67
 
102
68
  - A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
103
69
  system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
104
- `rpc.service=S3|KMS|SecretsManager|BedrockRuntime`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
70
+ `rpc.service=S3|KMS|SecretsManager|BedrockRuntime|AppConfigData`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
105
71
  Bedrock also `gen_ai.system=aws.bedrock`).
106
72
  - A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
107
73
  `messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
108
74
  `crypto.client.operation.*` (KMS), `secrets.client.operation.*` (Secrets Manager),
109
75
  `llm.client.operation.*` and `llm.client.token.usage` (Bedrock), `compute.client.operation.*`
110
- (Kubernetes Jobs),
76
+ (Kubernetes Jobs), `config.client.operation.*` (feature flags),
111
77
  `http.client.request.*` (HTTP, and the HTTP request
112
78
  under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
113
79
  - Redis, SQS, QStash, S3, Vercel Blob, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a `dependency.call_failed` warning on failure,
@@ -121,7 +87,7 @@ published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
121
87
  Never in this library's spans, metrics or logs: keys, URLs, bodies, message ids or object
122
88
  names; email addresses, subjects, bodies or tags; secret ids, version ids, stages and
123
89
  values; model ids, prompts and outputs; namespaces, Job names, manifests and service
124
- account tokens; KMS key ids, encryption contexts,
90
+ account tokens; feature flag names, values and AppConfig identifiers; KMS key ids, encryption contexts,
125
91
  plaintext, ciphertext or data keys. Use static dependency names such as `identity-provider` or `uploads`; never derive
126
92
  them from a tenant, user or request. All clients apply observability's identifier rule
127
93
  (1–96 letters, digits, `_`, `.` or `-`, starting with a letter or digit).
@@ -304,6 +270,33 @@ DELETE retry 502, 503, 504 and network failures once. 429, 5xx, timeouts and con
304
270
  failures raise `DependencyUnavailable`; other rejections raise `KubernetesApiError` with the
305
271
  status code and the API's reason, never its message.
306
272
 
273
+ A value the pod needs but that must not be readable in the Job's spec (a run token, say) goes
274
+ in a Secret the Job owns:
275
+
276
+ ```python
277
+ created = await jobs.create(manifest) # the container reads env from secretKeyRef
278
+ uid = created.uid or (await jobs.get(name)).uid # None in created when the Job existed
279
+ await jobs.create_secret(f"{name}-token", {"token": run_token}, job=name, job_uid=uid)
280
+ ```
281
+
282
+ ```yaml
283
+ env:
284
+ - name: RUN_TOKEN
285
+ valueFrom: { secretKeyRef: { name: cmd-4821-token, key: token } }
286
+ ```
287
+
288
+ The Secret is `Opaque` and `immutable`, with an owner reference to the Job's name and uid, so
289
+ the garbage collector deletes it with the Job (including through `ttlSecondsAfterFinished`).
290
+ The Job is created first because the reference needs its uid; the kubelet keeps retrying a
291
+ pod whose Secret does not exist yet. `create_secret` is idempotent by name like `create`: a
292
+ 409 `AlreadyExists` returns `created=False` and keeps the existing Secret without comparing or
293
+ updating it, so a retry must reuse the same value (or `delete_secret` and create again).
294
+ `delete_secret` returns `False` when there was none. `blockOwnerDeletion` is not set, so the
295
+ caller needs only `create` and `delete` on `secrets` (no `get`, and nothing on
296
+ `jobs/finalizers`). Secret names, data keys and values never reach telemetry, logs or error
297
+ messages; the request body is still in the chained httpx exception's `request`, so do not log
298
+ `__cause__` details.
299
+
307
300
  ### Bedrock
308
301
 
309
302
  ```python
@@ -342,6 +335,33 @@ copy when the same secret is read often. A missing secret, access denied, a valu
342
335
  decrypt or a secret scheduled for deletion keep their botocore type; throttling, 5xx and
343
336
  timeouts raise `DependencyUnavailable`. `repr(secret)` never shows the value.
344
337
 
338
+ ### Feature flags (AWS AppConfig)
339
+
340
+ Reads [AppConfig feature flags](https://aws.amazon.com/blogs/mt/using-aws-appconfig-feature-flags/)
341
+ through the AppConfig Data API and keeps a snapshot in memory; evaluating a flag is a lookup:
342
+
343
+ ```python
344
+ from clients_021.featureflags import FeatureFlags # the protocol, no extra needed
345
+ from clients_021.featureflags.appconfig import AppConfigFlags
346
+
347
+ flags: FeatureFlags = AppConfigFlags.create(
348
+ obs, settings.appconfig_app, settings.appconfig_env, settings.appconfig_profile,
349
+ region="us-east-1",
350
+ )
351
+ if await flags.is_enabled("new-checkout"): # unknown flag -> default=False
352
+ ...
353
+ limit = await flags.flag("rate-limit") # Flag(enabled=True, attributes={"rps": 50})
354
+ ```
355
+
356
+ The snapshot is refreshed by the first request after `refresh_seconds` (default 45, minimum
357
+ 15), one poll at a time. If a refresh fails while a snapshot exists, the stale one keeps
358
+ serving and the next attempt is 15 seconds later; with no snapshot yet the outage raises
359
+ `DependencyUnavailable`. A configuration that is not valid flag JSON raises `ValueError` on
360
+ the first load and is ignored with a warning afterwards. Supports plain flags and their
361
+ attributes; multi-variant flags and rules need the AppConfig Agent, which evaluates them.
362
+ The IAM role needs `appconfig:StartConfigurationSession` and
363
+ `appconfig:GetLatestConfiguration` on the configuration profile.
364
+
345
365
  ### KMS
346
366
 
347
367
  The encryption context is required on every call, so a ciphertext only decrypts for the
@@ -458,7 +478,7 @@ Retry policies are explicit:
458
478
  POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
459
479
  their original exception. HTTP status responses are returned for the caller to handle.
460
480
  - Resend sends each request once (POST); send an `idempotency_key` and retry in the caller.
461
- - AWS factories (SQS, S3, SES, KMS, Secrets Manager, Bedrock) use botocore standard retries with at most **three total
481
+ - AWS factories (SQS, S3, SES, KMS, Secrets Manager, Bedrock, feature flags) use botocore standard retries with at most **three total
462
482
  attempts**.
463
483
  Injected SDK clients keep their configured policy. SQS consumers must tolerate duplicate
464
484
  messages; a timeout does not prove that a send or write was not applied.
@@ -481,7 +501,7 @@ any operation**. Map it at the service boundary and apply the operation's idempo
481
501
  Its message and structured fields are safe to log when dependency names are static. Its
482
502
  chained SDK exception may contain sensitive provider data; do not log exception chains.
483
503
 
484
- S3, SQS, SES, KMS and Secrets Manager run the synchronous AWS SDK in worker threads. Cancelling the await does not
504
+ S3, SQS, SES, KMS, Secrets Manager and feature flags run the synchronous AWS SDK in worker threads. Cancelling the await does not
485
505
  stop an SDK request already running in its thread. Timeouts are per SDK/network attempt,
486
506
  not an overall deadline. Limit consumer concurrency and let in-flight work finish before
487
507
  closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "021-python-clients"
7
- version = "0.4.0"
8
- description = "Observed clients for Redis, SQS, QStash, S3, Vercel Blob, HTTP, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs, built on 021-python-observability"
7
+ version = "0.5.0"
8
+ description = "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"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.12"
13
- keywords = ["redis", "sqs", "qstash", "s3", "httpx", "email", "ses", "resend", "kms", "secrets-manager", "bedrock", "kubernetes", "opentelemetry", "observability"]
13
+ keywords = ["redis", "sqs", "qstash", "s3", "httpx", "email", "ses", "resend", "kms", "secrets-manager", "bedrock", "kubernetes", "appconfig", "feature-flags", "opentelemetry", "observability"]
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
16
16
  "Intended Audience :: Developers",
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Typing :: Typed",
25
25
  ]
26
26
  dependencies = [
27
- "021-python-observability>=0.3,<0.4",
27
+ "021-python-observability>=0.3,<0.5",
28
28
  "httpx>=0.28,<1",
29
29
  "opentelemetry-api>=1.44,<2",
30
30
  ]
@@ -51,7 +51,7 @@ dev = [
51
51
  "ruff>=0.16,<0.17",
52
52
  "pyright>=1.1.405",
53
53
  "moto[sqs,s3,ses,kms,secretsmanager]>=5.2,<6",
54
- "boto3-stubs[sqs,s3,sesv2,kms,secretsmanager,bedrock-runtime]>=1.40,<2",
54
+ "boto3-stubs[sqs,s3,sesv2,kms,secretsmanager,bedrock-runtime,appconfigdata]>=1.40,<2",
55
55
  ]
56
56
 
57
57
  [tool.hatch.build.targets.wheel]
@@ -4,7 +4,8 @@
4
4
  `clients_021.compute.kubernetes` need no extras;
5
5
  `clients_021.cache.redis` needs the `redis` extra; `clients_021.queue.sqs`,
6
6
  `clients_021.storage.s3`, `clients_021.email.ses`, `clients_021.crypto.kms`,
7
- `clients_021.secrets.secrets_manager` and `clients_021.llm.bedrock` the `aws` extra;
7
+ `clients_021.secrets.secrets_manager`, `clients_021.llm.bedrock` and
8
+ `clients_021.featureflags.appconfig` the `aws` extra (`clients_021.featureflags` needs none);
8
9
  `clients_021.http.httpx2` the `httpx2` extra.
9
10
  `clients_021.storage.vercel_blob` needs the `vercel-blob` extra.
10
11
  """
@@ -2,11 +2,11 @@
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, Vercel Blob), `crypto` (KMS), `secrets` (Secrets Manager), `llm` (Bedrock) and
6
- `compute` (Kubernetes Jobs) get their own pair on the same meter. `Call` is the same
7
- measurement for synchronous SDK calls and for operations that end later than they start, such
8
- as a model response stream. Attributes are bounded: dependency, operation, status and
9
- error.type; never keys, URLs, bodies or ids.
5
+ `storage` (S3, Vercel Blob), `crypto` (KMS), `secrets` (Secrets Manager), `llm` (Bedrock),
6
+ `compute` (Kubernetes Jobs) and `config` (feature flags) get their own pair on the same
7
+ meter. `Call` is the same measurement for synchronous SDK calls and for operations that end
8
+ later than they start, such as a model response stream. Attributes are bounded: dependency,
9
+ operation, status and error.type; never keys, URLs, bodies or ids.
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -24,7 +24,7 @@ from opentelemetry.trace import SpanKind, StatusCode
24
24
 
25
25
  from clients_021.errors import DependencyUnavailable
26
26
 
27
- Family = Literal["db", "messaging", "storage", "crypto", "secrets", "llm", "compute"]
27
+ Family = Literal["db", "messaging", "storage", "crypto", "secrets", "llm", "compute", "config"]
28
28
  # Returns the reason when an exception means "the dependency failed", None for a bug.
29
29
  Classifier = Callable[[BaseException], str | None]
30
30
 
@@ -37,6 +37,7 @@ _UNITS = {
37
37
  "secrets": "{operation}",
38
38
  "llm": "{operation}",
39
39
  "compute": "{operation}",
40
+ "config": "{operation}",
40
41
  }
41
42
  _cache: WeakKeyDictionary[Observability, dict[Family, tuple[Counter, Histogram]]] = (
42
43
  WeakKeyDictionary()