021-python-clients 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {021_python_clients-0.2.0.dist-info → 021_python_clients-0.4.0.dist-info}/METADATA +141 -24
- {021_python_clients-0.2.0.dist-info → 021_python_clients-0.4.0.dist-info}/RECORD +19 -10
- clients_021/__init__.py +5 -3
- clients_021/_observe.py +90 -4
- clients_021/compute/__init__.py +1 -0
- clients_021/compute/kubernetes.py +314 -0
- clients_021/email/resend.py +5 -1
- clients_021/http/__init__.py +11 -0
- clients_021/http/_observe.py +175 -0
- clients_021/http/httpx.py +280 -0
- clients_021/http/httpx2.py +76 -0
- clients_021/httpx2.py +3 -91
- clients_021/llm/__init__.py +1 -0
- clients_021/llm/bedrock.py +250 -0
- clients_021/queue/qstash.py +1 -1
- clients_021/secrets/__init__.py +1 -0
- clients_021/secrets/secrets_manager.py +127 -0
- clients_021/http.py +0 -131
- {021_python_clients-0.2.0.dist-info → 021_python_clients-0.4.0.dist-info}/WHEEL +0 -0
- {021_python_clients-0.2.0.dist-info → 021_python_clients-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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.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
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: bedrock,email,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
|
|
@@ -48,8 +48,8 @@ uv add "021-python-clients[redis,aws,httpx2,vercel-blob]" # pick the extras yo
|
|
|
48
48
|
|
|
49
49
|
| Module | Extra | What it gives |
|
|
50
50
|
| --- | --- | --- |
|
|
51
|
-
| `clients_021.http` | none | `HttpClient`:
|
|
52
|
-
| `clients_021.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
51
|
+
| `clients_021.http.httpx` | none | `HttpClient`: buffered requests, streaming and full-body telemetry; native `ObservedTransport` |
|
|
52
|
+
| `clients_021.http.httpx2` | `httpx2` | `ObservedTransport` for SDKs built on httpx2 (Anthropic, OpenAI) |
|
|
53
53
|
| `clients_021.cache.redis` | `redis` | `RedisClient`: JSON with TTL, one-time reads, windowed counters, ping |
|
|
54
54
|
| `clients_021.queue.qstash` | none | `QStashClient`: JSON publishing, deduplication, delivery options and forwarded trace context |
|
|
55
55
|
| `clients_021.queue.sqs` | `aws` | `SqsClient`: send, receive, delete, visibility, trace carried in the message |
|
|
@@ -58,6 +58,9 @@ 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, in-cluster service account |
|
|
63
|
+
| `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
64
|
|
|
62
65
|
Every client takes the service's `Observability` (from `setup_observability`) and a
|
|
63
66
|
`dependency` name, which becomes the `dependency` label of its metrics.
|
|
@@ -66,15 +69,20 @@ Every client takes the service's `Observability` (from `setup_observability`) an
|
|
|
66
69
|
|
|
67
70
|
Providers with the same purpose live under one capability package: `email/` contains
|
|
68
71
|
Resend and SES; `queue/` contains SQS and QStash; `storage/` contains S3 and Vercel Blob; `cache/`
|
|
69
|
-
contains Redis; `crypto/` contains KMS
|
|
70
|
-
explicitly:
|
|
72
|
+
contains Redis; `crypto/` contains KMS; `secrets/` contains Secrets Manager; `llm/` contains Bedrock; `compute/` contains Kubernetes Jobs. `http/` contains the httpx client and httpx2
|
|
73
|
+
transport. Import providers explicitly:
|
|
71
74
|
|
|
72
75
|
```python
|
|
76
|
+
from clients_021.http import HttpClient
|
|
77
|
+
from clients_021.http.httpx2 import ObservedTransport # requires the httpx2 extra
|
|
73
78
|
from clients_021.queue.sqs import SqsClient
|
|
74
79
|
from clients_021.queue.qstash import QStashClient
|
|
75
80
|
from clients_021.storage.s3 import S3Client
|
|
76
81
|
from clients_021.cache.redis import RedisClient
|
|
77
82
|
from clients_021.crypto.kms import KmsClient
|
|
83
|
+
from clients_021.secrets.secrets_manager import SecretsManagerClient
|
|
84
|
+
from clients_021.llm.bedrock import BedrockRuntime
|
|
85
|
+
from clients_021.compute.kubernetes import KubernetesJobsClient
|
|
78
86
|
```
|
|
79
87
|
|
|
80
88
|
SQS supports polling, receipts and visibility changes; QStash delivers to an HTTP endpoint.
|
|
@@ -85,26 +93,35 @@ does not load the optional AWS SDK.
|
|
|
85
93
|
|
|
86
94
|
The 0.1.0 imports `clients_021.redis`, `clients_021.s3` and `clients_021.kms` remain
|
|
87
95
|
compatible aliases for the same classes and result types. Importing a capability package
|
|
88
|
-
does not load its optional SDKs.
|
|
96
|
+
does not load its optional SDKs. `clients_021.HttpClient` and `clients_021.http` keep their
|
|
97
|
+
published exports; `clients_021.httpx2.ObservedTransport` remains an alias for
|
|
98
|
+
`clients_021.http.httpx2.ObservedTransport`.
|
|
89
99
|
|
|
90
100
|
## What every call gets
|
|
91
101
|
|
|
92
102
|
- A CLIENT span named `<dependency> <operation>` (`HTTP <dependency>` for HTTP), with the
|
|
93
103
|
system attribute (`db.system.name=redis`, `messaging.system=aws_sqs|resend|aws_ses`,
|
|
94
|
-
`rpc.service=S3|KMS`, `rpc.system=vercel-blob`
|
|
104
|
+
`rpc.service=S3|KMS|SecretsManager|BedrockRuntime`, `rpc.system=vercel-blob`, `rpc.system=kubernetes`;
|
|
105
|
+
Bedrock also `gen_ai.system=aws.bedrock`).
|
|
95
106
|
- A counter and a duration histogram: `db.client.operation.*` (Redis, shared with SQL),
|
|
96
107
|
`messaging.client.operation.*` (SQS, QStash, email), `storage.client.operation.*` (S3, Vercel Blob),
|
|
97
|
-
`crypto.client.operation.*` (KMS), `
|
|
108
|
+
`crypto.client.operation.*` (KMS), `secrets.client.operation.*` (Secrets Manager),
|
|
109
|
+
`llm.client.operation.*` and `llm.client.token.usage` (Bedrock), `compute.client.operation.*`
|
|
110
|
+
(Kubernetes Jobs),
|
|
111
|
+
`http.client.request.*` (HTTP, and the HTTP request
|
|
98
112
|
under QStash and Resend). Labels are bounded; see [CONTRACT.md](CONTRACT.md).
|
|
99
|
-
- Redis, SQS, QStash, S3, Vercel Blob, email and
|
|
100
|
-
with dependency, operation and error type only. HTTP
|
|
113
|
+
- Redis, SQS, QStash, S3, Vercel Blob, email, KMS, Secrets Manager, Bedrock and Kubernetes Jobs emit a `dependency.call_failed` warning on failure,
|
|
114
|
+
with dependency, operation and error type only. HTTP/httpx2 also log failed attempts,
|
|
115
|
+
including body failures and cancellation; QStash/Resend suppress the nested HTTP log.
|
|
101
116
|
- `DependencyUnavailable(dependency, operation, reason)` when the service timed out,
|
|
102
117
|
refused the connection, throttled or failed with a 5xx. Map it to a 503. Errors that mean
|
|
103
118
|
the request is wrong (access denied, a missing bucket, a wrong Redis type) keep their
|
|
104
119
|
original type: they are bugs, not outages.
|
|
105
120
|
|
|
106
121
|
Never in this library's spans, metrics or logs: keys, URLs, bodies, message ids or object
|
|
107
|
-
names; email addresses, subjects, bodies or tags;
|
|
122
|
+
names; email addresses, subjects, bodies or tags; secret ids, version ids, stages and
|
|
123
|
+
values; model ids, prompts and outputs; namespaces, Job names, manifests and service
|
|
124
|
+
account tokens; KMS key ids, encryption contexts,
|
|
108
125
|
plaintext, ciphertext or data keys. Use static dependency names such as `identity-provider` or `uploads`; never derive
|
|
109
126
|
them from a tenant, user or request. All clients apply observability's identifier rule
|
|
110
127
|
(1–96 letters, digits, `_`, `.` or `-`, starting with a letter or digit).
|
|
@@ -267,6 +284,64 @@ except EmailRejected as rejected: # the address, the domain or the credentials;
|
|
|
267
284
|
SES has no idempotency key (the argument is ignored): deduplicate in the caller when a retry
|
|
268
285
|
must not send twice. Tags are for provider analytics; never put personal data in them.
|
|
269
286
|
|
|
287
|
+
### Kubernetes Jobs
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from clients_021.compute.kubernetes import KubernetesJobsClient
|
|
291
|
+
|
|
292
|
+
jobs = KubernetesJobsClient.in_cluster(obs, namespace="agent-runs")
|
|
293
|
+
created = await jobs.create({"metadata": {"name": "cmd-4821"}, "spec": job_spec})
|
|
294
|
+
status = await jobs.get("cmd-4821") # JobStatus(phase="running", ...) or None
|
|
295
|
+
await jobs.delete("cmd-4821", propagation="Background")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
No SDK: it calls the `batch/v1` REST API through `HttpClient`. `in_cluster` uses the pod's
|
|
299
|
+
service account (CA bundle, namespace, and a token it re-reads at most once a minute because
|
|
300
|
+
projected tokens rotate). `create` is idempotent by name: `generateName` is refused, and an
|
|
301
|
+
existing Job returns `created=False`, so a caller can repeat it after a timeout. `get`
|
|
302
|
+
returns `None` and `delete` returns `False` for a missing Job. POST is sent once; GET and
|
|
303
|
+
DELETE retry 502, 503, 504 and network failures once. 429, 5xx, timeouts and connection
|
|
304
|
+
failures raise `DependencyUnavailable`; other rejections raise `KubernetesApiError` with the
|
|
305
|
+
status code and the API's reason, never its message.
|
|
306
|
+
|
|
307
|
+
### Bedrock
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from clients_021.llm.bedrock import BedrockRuntime
|
|
311
|
+
from langchain_aws import ChatBedrockConverse
|
|
312
|
+
|
|
313
|
+
runtime = BedrockRuntime.create(obs, region="us-east-1")
|
|
314
|
+
model = ChatBedrockConverse(model=settings.inference_profile, client=runtime)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`BedrockRuntime` keeps boto3's call signatures for `converse`, `converse_stream`,
|
|
318
|
+
`invoke_model`, `invoke_model_with_response_stream` and `count_tokens`, and passes every
|
|
319
|
+
other attribute (`meta`, `exceptions`) through to the boto3 client, so an SDK can take it
|
|
320
|
+
where it expects one. Calls stay synchronous like boto3's. A streaming call is measured
|
|
321
|
+
until its event stream ends; leaving the loop early or calling `close()` ends it as `ok`.
|
|
322
|
+
Converse and ConverseStream add the reported input, output and cache tokens to
|
|
323
|
+
`llm.client.token.usage`. Throttling (also mid-stream), 5xx, a model still loading and the
|
|
324
|
+
stream errors Bedrock documents as retryable raise `DependencyUnavailable`; access denied,
|
|
325
|
+
validation errors, a missing model and quota limits keep their botocore type. A retried
|
|
326
|
+
model call is billed again.
|
|
327
|
+
|
|
328
|
+
### Secrets Manager
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from clients_021.secrets.secrets_manager import SecretsManagerClient
|
|
332
|
+
|
|
333
|
+
secrets = SecretsManagerClient.create(obs, region="us-east-1")
|
|
334
|
+
secret = await secrets.get("connections/brain")
|
|
335
|
+
token = secret.text() # SecretString; secret.binary() for SecretBinary
|
|
336
|
+
previous = await secrets.get("connections/brain", version_stage="AWSPREVIOUS")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`get` reads the current version unless a `version_id` or a `version_stage` is given (not
|
|
340
|
+
both). The client does not cache: every call is a billed request, so keep a short-lived
|
|
341
|
+
copy when the same secret is read often. A missing secret, access denied, a value KMS cannot
|
|
342
|
+
decrypt or a secret scheduled for deletion keep their botocore type; throttling, 5xx and
|
|
343
|
+
timeouts raise `DependencyUnavailable`. `repr(secret)` never shows the value.
|
|
344
|
+
|
|
270
345
|
### KMS
|
|
271
346
|
|
|
272
347
|
The encryption context is required on every call, so a ciphertext only decrypts for the
|
|
@@ -290,21 +365,58 @@ ciphertext keep their botocore type (they are bugs or attacks, not outages); a t
|
|
|
290
365
|
envelope raises `cryptography`'s `InvalidTag`. The envelope layout is in
|
|
291
366
|
[CONTRACT.md](CONTRACT.md).
|
|
292
367
|
|
|
368
|
+
### HTTP bodies and streaming
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from clients_021.http import HttpClient
|
|
372
|
+
|
|
373
|
+
http = HttpClient(obs, timeout_seconds=10)
|
|
374
|
+
try:
|
|
375
|
+
async with http.stream("GET", download_url, dependency="documents") as response:
|
|
376
|
+
response.raise_for_status()
|
|
377
|
+
async for chunk in response.aiter_bytes():
|
|
378
|
+
await write_chunk(chunk)
|
|
379
|
+
finally:
|
|
380
|
+
await http.aclose()
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
These async clients run on asyncio. `request()` returns a buffered `HttpResponse`; `stream()` yields a native httpx response
|
|
384
|
+
without buffering and accepts an async iterable as upload `content`. Streaming sends once,
|
|
385
|
+
even for GET. The buffered client's configured idempotent retries are unchanged. Close the
|
|
386
|
+
response context before closing the client. For SDKs using native httpx, inject
|
|
387
|
+
`clients_021.http.httpx.ObservedTransport`; httpx2 SDKs use the transport below.
|
|
388
|
+
|
|
389
|
+
Each HTTP attempt emits one span, counter and duration after body EOF and connection close,
|
|
390
|
+
or early close. This includes upload, download, pauses between chunks and cleanup. Body
|
|
391
|
+
errors after 200 headers are recorded as errors. `http.response.body.complete` distinguishes
|
|
392
|
+
EOF from partial/unread close; it does not mean the status or cleanup succeeded. Intentional
|
|
393
|
+
partial close is INFO `dependency.stream_closed`; exceptions, cancellation and HTTP >= 400
|
|
394
|
+
produce WARNING `dependency.call_failed`. No success log, URL, header, body or provider
|
|
395
|
+
error message is emitted. `log_failures=False` disables attempt logs when an enclosing
|
|
396
|
+
operation owns the warning, as QStash and Resend do.
|
|
397
|
+
|
|
398
|
+
These duration semantics and the completion label change the 0.2.x telemetry contract;
|
|
399
|
+
see [migration notes](CONTRACT.md#migration-from-02x) before updating dashboards or alerts.
|
|
400
|
+
|
|
293
401
|
### SDKs on httpx2
|
|
294
402
|
|
|
295
403
|
For an SDK on httpx2:
|
|
296
404
|
|
|
297
405
|
```python
|
|
298
406
|
from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient
|
|
299
|
-
from clients_021.httpx2 import ObservedTransport
|
|
407
|
+
from clients_021.http.httpx2 import ObservedTransport
|
|
300
408
|
|
|
301
409
|
transport = ObservedTransport(obs, dependency="anthropic")
|
|
302
410
|
claude = AsyncAnthropic(http_client=DefaultAsyncHttpxClient(transport=transport))
|
|
303
411
|
```
|
|
304
412
|
|
|
305
|
-
It does not
|
|
413
|
+
It does not inject trace context into third-party API requests unless `propagate=True`.
|
|
306
414
|
The transport preserves SDK exceptions so the SDK retains control of retries and error
|
|
307
|
-
handling; it does not convert them to `DependencyUnavailable`.
|
|
415
|
+
handling; it does not convert them to `DependencyUnavailable`. Its span stays open through
|
|
416
|
+
incremental response reads and close. Use the SDK's stream context manager, or close manual
|
|
417
|
+
responses explicitly; abandoning a stream leaves both its connection and span open. Caller
|
|
418
|
+
processing keeps its own trace context, including when streams are interleaved or consumed
|
|
419
|
+
in another task.
|
|
308
420
|
|
|
309
421
|
## Service integration
|
|
310
422
|
|
|
@@ -333,18 +445,20 @@ async with AsyncExitStack() as resources:
|
|
|
333
445
|
resources.push_async_callback(email.aclose)
|
|
334
446
|
keys = KmsClient.create(obs, settings.session_key_arn, region="us-east-1")
|
|
335
447
|
resources.push_async_callback(keys.aclose)
|
|
448
|
+
secrets = SecretsManagerClient.create(obs, region="us-east-1")
|
|
449
|
+
resources.push_async_callback(secrets.aclose)
|
|
336
450
|
# Run the service here. Stop its background workers before leaving this scope.
|
|
337
451
|
```
|
|
338
452
|
|
|
339
453
|
Retry policies are explicit:
|
|
340
454
|
|
|
341
|
-
- HTTP retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
455
|
+
- Buffered HTTP (`request`) retries GET, HEAD, OPTIONS, PUT and DELETE on network failures, timeouts,
|
|
342
456
|
remote protocol/proxy connection failures and 502/503/504. `retries=2` means at most
|
|
343
457
|
three attempts.
|
|
344
458
|
POST and PATCH run once. Invalid URLs/protocols and invalid request construction keep
|
|
345
459
|
their original exception. HTTP status responses are returned for the caller to handle.
|
|
346
460
|
- Resend sends each request once (POST); send an `idempotency_key` and retry in the caller.
|
|
347
|
-
- AWS factories (SQS, S3, SES, KMS) use botocore standard retries with at most **three total
|
|
461
|
+
- AWS factories (SQS, S3, SES, KMS, Secrets Manager, Bedrock) use botocore standard retries with at most **three total
|
|
348
462
|
attempts**.
|
|
349
463
|
Injected SDK clients keep their configured policy. SQS consumers must tolerate duplicate
|
|
350
464
|
messages; a timeout does not prove that a send or write was not applied.
|
|
@@ -367,15 +481,16 @@ any operation**. Map it at the service boundary and apply the operation's idempo
|
|
|
367
481
|
Its message and structured fields are safe to log when dependency names are static. Its
|
|
368
482
|
chained SDK exception may contain sensitive provider data; do not log exception chains.
|
|
369
483
|
|
|
370
|
-
S3, SQS, SES and
|
|
484
|
+
S3, SQS, SES, KMS and Secrets Manager run the synchronous AWS SDK in worker threads. Cancelling the await does not
|
|
371
485
|
stop an SDK request already running in its thread. Timeouts are per SDK/network attempt,
|
|
372
486
|
not an overall deadline. Limit consumer concurrency and let in-flight work finish before
|
|
373
487
|
closing the clients. Redis window counters require Redis server 7 or newer (`EXPIRE NX`).
|
|
374
488
|
|
|
375
|
-
HTTP
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
489
|
+
HTTP spans now include network body consumption and cleanup. The SDK/application still
|
|
490
|
+
owns content decoding, JSON/SSE interpretation and processing errors outside raw-stream I/O.
|
|
491
|
+
Timeouts remain per I/O, not a total stream deadline. Cancellation waits for shielded body
|
|
492
|
+
cleanup before propagating. Close all responses and consumers before shutdown/flush.
|
|
493
|
+
S3 `get` buffers the entire object in memory; use a dedicated streaming adapter for large files.
|
|
379
494
|
|
|
380
495
|
## Development
|
|
381
496
|
|
|
@@ -390,6 +505,8 @@ deduplication, start the [official local server](https://upstash.com/docs/qstash
|
|
|
390
505
|
set its `QSTASH_URL` and `QSTASH_TOKEN`, and run `uv run python tests/qstash_local.py`.
|
|
391
506
|
That smoke test accepts only a loopback QStash server and binds its destination dynamically.
|
|
392
507
|
|
|
393
|
-
|
|
508
|
+
Kubernetes Jobs tests use httpx's `MockTransport`. Bedrock tests use botocore's Stubber and
|
|
509
|
+
fake event streams. SQS, S3, SES, KMS and Secrets
|
|
510
|
+
Manager tests run against [moto](https://github.com/getmoto/moto) (plus
|
|
394
511
|
botocore's Stubber where moto ignores a field or differs from AWS); Resend tests use httpx's
|
|
395
512
|
`MockTransport`; Redis tests need a real Redis. Licensed under Apache-2.0.
|
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
clients_021/__init__.py,sha256=
|
|
1
|
+
clients_021/__init__.py,sha256=D7i865mBaxw32ycKWsMazMRljtYFdabdSG8dHjfWg3M,921
|
|
2
2
|
clients_021/_aws.py,sha256=sj2kCfod-vz9J93UyeVzNVLGK3M25yipKdW15KyIkw4,1722
|
|
3
|
-
clients_021/_observe.py,sha256=
|
|
3
|
+
clients_021/_observe.py,sha256=Kh-2gitT2bI4vNa-Y7PYsCns60XJzJ_N7ZbTuyX82lo,6977
|
|
4
4
|
clients_021/errors.py,sha256=GS_fN8mjr7MCOkhqPhH6LweIvQ20Vd29U-aZY8zBeJQ,972
|
|
5
|
-
clients_021/
|
|
6
|
-
clients_021/httpx2.py,sha256=w248IcEtDZtsEDkkvj4Bpfl7MU7IJ08EHqHWhryC_3k,3880
|
|
5
|
+
clients_021/httpx2.py,sha256=rNbNGJuPJPZUwVbNN5L88IF0g3Tq6EHqj_uFmJ3RmLQ,161
|
|
7
6
|
clients_021/kms.py,sha256=YcZXURpqIZTkjcP0fpWMD6X_adMAYwN7zAJNhUImR4s,183
|
|
8
7
|
clients_021/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
8
|
clients_021/redis.py,sha256=xC3tji_ldJAwaBntkZTuPhNHLIzl3F3nDzkikMrbJCc,169
|
|
10
9
|
clients_021/s3.py,sha256=6Q5_1emngSB00IwvBdrmV3iAg-Q_Ndco1HGzYObL5FI,231
|
|
11
10
|
clients_021/cache/__init__.py,sha256=3OyQQ-uZeghpgJvvJ2k56_t50q3CCwbAp3dBrCMMcSU,76
|
|
12
11
|
clients_021/cache/redis.py,sha256=DRXCMc8MqY8KsZ50j4X4mUsd0lvvmP-UDuUD2wJXyW8,4932
|
|
12
|
+
clients_021/compute/__init__.py,sha256=PP23LVKxoJGkwzzFbuKMRd35LugdYqJWElp9lWymllM,78
|
|
13
|
+
clients_021/compute/kubernetes.py,sha256=a0-9ldOg1mQjJBVzWAhZH4QGEN6qAzcgl_n8Ty0rSts,12456
|
|
13
14
|
clients_021/crypto/__init__.py,sha256=qESFBPQBKcHhdFXqeJzSyEP41yveG7rFrxnpITmqlNU,77
|
|
14
15
|
clients_021/crypto/kms.py,sha256=My5Xyg0bOiT9YHYWojQSszpREEh3bSYMMB26wy6fNEI,7261
|
|
15
16
|
clients_021/email/__init__.py,sha256=ivmdu9hhNDwPqBn3G7uNG0Zpc3DEMnlnWBVVoVQ-fQA,790
|
|
16
17
|
clients_021/email/_message.py,sha256=ulTX2cXPxSVaq_5FVF0e6glePVJ7wiO5P3M4WUWN3N0,3258
|
|
17
|
-
clients_021/email/resend.py,sha256=
|
|
18
|
+
clients_021/email/resend.py,sha256=1EjvEbNbbVDQs5TZfYuZrqh14io8cb5ADneNA63DQbg,5416
|
|
18
19
|
clients_021/email/ses.py,sha256=0ukHvuVE0nTU5mYWRy2EaHY5v9dUwi4GSKES93pjqBg,4668
|
|
20
|
+
clients_021/http/__init__.py,sha256=P4OmYruhkQfJwJzHA90FXKWg_A2rh5iOl-ES_NTMMVQ,311
|
|
21
|
+
clients_021/http/_observe.py,sha256=hpTzcej9MhlvM-RgUZWicR57ulE90GmZMs5U_qFoquQ,6922
|
|
22
|
+
clients_021/http/httpx.py,sha256=gvqSpO1rb6nBA5i01W1yos5mPBwLqa3-HDYwMtQXc6s,10627
|
|
23
|
+
clients_021/http/httpx2.py,sha256=fmqH4bikDKZ6YY0sNBHZbocff2zU40Gpks1aoRuOGeQ,2810
|
|
24
|
+
clients_021/llm/__init__.py,sha256=pEI4drpN0MKg_gp8BP1U7kpVd3NDV_Sa9eFD3J87LPw,86
|
|
25
|
+
clients_021/llm/bedrock.py,sha256=GJafCh6PhBrsDgs_GapoKiVflv32V7BUqyAj52pMQiA,9367
|
|
19
26
|
clients_021/queue/__init__.py,sha256=CesweOrXmX5cyU-mRvqu-4wLkNJI15zi_Lows6SPupY,338
|
|
20
|
-
clients_021/queue/qstash.py,sha256=
|
|
27
|
+
clients_021/queue/qstash.py,sha256=NndlqmNkcBgs0ExGsT-3-bMAdU0skO8Djptqj8NoJAM,8335
|
|
21
28
|
clients_021/queue/sqs.py,sha256=jnccgbxyglzs2mR_DsH04IisEwygDlw6s3RxR0UtMTc,6693
|
|
29
|
+
clients_021/secrets/__init__.py,sha256=LdxphQvruITeP5jXZR69ly2jxMxuHCI1BDavw2mU0EY,76
|
|
30
|
+
clients_021/secrets/secrets_manager.py,sha256=ttP3_vCXRtOYvbUaNIkFlSTzYYr4yU3Uwim3juXUfPM,4746
|
|
22
31
|
clients_021/storage/__init__.py,sha256=KrJViRHBK7PkIYnagg7vyXp95SzMFp9cUCKEnMeFr84,78
|
|
23
32
|
clients_021/storage/s3.py,sha256=NDtG9QzHSXvrcazTYTq1EH5VjuRd2xMMsMvaumqci4Q,5946
|
|
24
33
|
clients_021/storage/vercel_blob.py,sha256=FlnoKiahqtk3PpLsGpyu_Gy87otg15i4BlJZsIB3LfM,6511
|
|
25
|
-
021_python_clients-0.
|
|
26
|
-
021_python_clients-0.
|
|
27
|
-
021_python_clients-0.
|
|
28
|
-
021_python_clients-0.
|
|
34
|
+
021_python_clients-0.4.0.dist-info/METADATA,sha256=Q4ITtnJ9tRJxxH3gdoE-1nl3A4ClHkAV5yihBGN1JTg,27808
|
|
35
|
+
021_python_clients-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
36
|
+
021_python_clients-0.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
37
|
+
021_python_clients-0.4.0.dist-info/RECORD,,
|
clients_021/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Observed clients for the external services Python services call.
|
|
2
2
|
|
|
3
|
-
`clients_021.http`, `clients_021.queue.qstash
|
|
3
|
+
`clients_021.http`, `clients_021.queue.qstash`, `clients_021.email` (Resend) and
|
|
4
|
+
`clients_021.compute.kubernetes` need no extras;
|
|
4
5
|
`clients_021.cache.redis` needs the `redis` extra; `clients_021.queue.sqs`,
|
|
5
|
-
`clients_021.storage.s3`, `clients_021.email.ses
|
|
6
|
-
`clients_021.
|
|
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;
|
|
8
|
+
`clients_021.http.httpx2` the `httpx2` extra.
|
|
7
9
|
`clients_021.storage.vercel_blob` needs the `vercel-blob` extra.
|
|
8
10
|
"""
|
|
9
11
|
|
clients_021/_observe.py
CHANGED
|
@@ -2,8 +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)
|
|
6
|
-
|
|
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.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
from __future__ import annotations
|
|
@@ -12,7 +15,7 @@ import asyncio
|
|
|
12
15
|
import time
|
|
13
16
|
from collections.abc import AsyncGenerator, Callable
|
|
14
17
|
from contextlib import asynccontextmanager
|
|
15
|
-
from typing import Literal
|
|
18
|
+
from typing import Literal, NoReturn
|
|
16
19
|
from weakref import WeakKeyDictionary
|
|
17
20
|
|
|
18
21
|
from observability_021 import Observability, get_logger
|
|
@@ -21,7 +24,7 @@ from opentelemetry.trace import SpanKind, StatusCode
|
|
|
21
24
|
|
|
22
25
|
from clients_021.errors import DependencyUnavailable
|
|
23
26
|
|
|
24
|
-
Family = Literal["db", "messaging", "storage", "crypto"]
|
|
27
|
+
Family = Literal["db", "messaging", "storage", "crypto", "secrets", "llm", "compute"]
|
|
25
28
|
# Returns the reason when an exception means "the dependency failed", None for a bug.
|
|
26
29
|
Classifier = Callable[[BaseException], str | None]
|
|
27
30
|
|
|
@@ -31,6 +34,9 @@ _UNITS = {
|
|
|
31
34
|
"messaging": "{operation}",
|
|
32
35
|
"storage": "{operation}",
|
|
33
36
|
"crypto": "{operation}",
|
|
37
|
+
"secrets": "{operation}",
|
|
38
|
+
"llm": "{operation}",
|
|
39
|
+
"compute": "{operation}",
|
|
34
40
|
}
|
|
35
41
|
_cache: WeakKeyDictionary[Observability, dict[Family, tuple[Counter, Histogram]]] = (
|
|
36
42
|
WeakKeyDictionary()
|
|
@@ -104,3 +110,83 @@ async def observed(
|
|
|
104
110
|
finally:
|
|
105
111
|
count.add(1, attributes)
|
|
106
112
|
duration.record(time.perf_counter() - started, attributes)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Call:
|
|
116
|
+
"""One observed operation started now and finished by `ok`, `fail` or `cancelled`.
|
|
117
|
+
|
|
118
|
+
The synchronous counterpart of `observed`, with the same span, instruments, labels and
|
|
119
|
+
warning. It also covers operations that outlive the call that started them: a stream is
|
|
120
|
+
finished when it is exhausted, fails or is closed. Finishing twice is ignored.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
obs: Observability,
|
|
126
|
+
*,
|
|
127
|
+
family: Family,
|
|
128
|
+
system: dict[str, str],
|
|
129
|
+
dependency: str,
|
|
130
|
+
operation: str,
|
|
131
|
+
classify: Classifier,
|
|
132
|
+
) -> None:
|
|
133
|
+
self._attributes: dict[str, str] = {"dependency": dependency, "operation": operation}
|
|
134
|
+
self._count, self._duration = instruments(obs, family)
|
|
135
|
+
self._classify = classify
|
|
136
|
+
self._started = time.perf_counter()
|
|
137
|
+
self._span = obs.tracer.start_span(
|
|
138
|
+
f"{dependency} {operation}",
|
|
139
|
+
kind=SpanKind.CLIENT,
|
|
140
|
+
attributes={**system, **self._attributes},
|
|
141
|
+
record_exception=False,
|
|
142
|
+
set_status_on_exception=False,
|
|
143
|
+
)
|
|
144
|
+
self._done = False
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def done(self) -> bool:
|
|
148
|
+
return self._done
|
|
149
|
+
|
|
150
|
+
def _finish(self) -> None:
|
|
151
|
+
self._done = True
|
|
152
|
+
self._span.set_attributes(self._attributes)
|
|
153
|
+
self._span.end()
|
|
154
|
+
self._count.add(1, self._attributes)
|
|
155
|
+
self._duration.record(time.perf_counter() - self._started, self._attributes)
|
|
156
|
+
|
|
157
|
+
def ok(self) -> None:
|
|
158
|
+
if self._done:
|
|
159
|
+
return
|
|
160
|
+
self._attributes["status"] = "ok"
|
|
161
|
+
self._finish()
|
|
162
|
+
|
|
163
|
+
def cancelled(self) -> None:
|
|
164
|
+
if self._done:
|
|
165
|
+
return
|
|
166
|
+
self._attributes["status"] = "error"
|
|
167
|
+
self._attributes["error.type"] = "cancelled"
|
|
168
|
+
self._span.set_status(StatusCode.ERROR)
|
|
169
|
+
self._finish()
|
|
170
|
+
|
|
171
|
+
def fail(self, error: BaseException) -> NoReturn:
|
|
172
|
+
"""Records the failure, then raises DependencyUnavailable or the error itself."""
|
|
173
|
+
if isinstance(error, asyncio.CancelledError | KeyboardInterrupt | GeneratorExit):
|
|
174
|
+
self.cancelled()
|
|
175
|
+
raise error
|
|
176
|
+
reason = self._classify(error)
|
|
177
|
+
if not self._done:
|
|
178
|
+
self._attributes["status"] = "error"
|
|
179
|
+
self._attributes["error.type"] = reason or type(error).__name__
|
|
180
|
+
self._span.set_status(StatusCode.ERROR)
|
|
181
|
+
logger.warning(
|
|
182
|
+
"dependency.call_failed",
|
|
183
|
+
dependency=self._attributes["dependency"],
|
|
184
|
+
operation=self._attributes["operation"],
|
|
185
|
+
error_type=self._attributes["error.type"],
|
|
186
|
+
)
|
|
187
|
+
self._finish()
|
|
188
|
+
if reason is None:
|
|
189
|
+
raise error
|
|
190
|
+
raise DependencyUnavailable(
|
|
191
|
+
self._attributes["dependency"], self._attributes["operation"], reason
|
|
192
|
+
) from error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Compute clients. Import providers explicitly; SDKs are optional extras."""
|