grpc-client-kit 0.1.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/CHANGELOG.md +14 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/PKG-INFO +23 -5
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/README.md +16 -4
- grpc_client_kit-0.3.0/grpc_client_kit/__version__.py +1 -0
- grpc_client_kit-0.3.0/grpc_client_kit/dishka.py +183 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/factory.py +47 -15
- grpc_client_kit-0.3.0/grpc_client_kit/metrics.py +229 -0
- grpc_client_kit-0.3.0/grpc_client_kit/settings.py +418 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/pyproject.toml +8 -1
- grpc_client_kit-0.1.2/grpc_client_kit/__version__.py +0 -1
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/.gitignore +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/LICENSE +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/__init__.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/balancers.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/channel.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/client.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/config.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/deadline.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/errors.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/health.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/__init__.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/base.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/circuit_breaker.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/client_logging.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/context.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/deadline.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/metrics.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/outlier.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/retry.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/timeout.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/tracing.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/interceptors/wait_for_ready.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/protocols.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/py.typed +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/utils.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.3.0}/grpc_client_kit/validation.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/bedrock-python/grpc-client-kit/compare/grpc-client-kit-v0.2.0...grpc-client-kit-v0.3.0) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Prometheus collector for the metrics protocol and Dishka providers for the client lifecycle ([6dfdb44](https://github.com/bedrock-python/grpc-client-kit/commit/6dfdb44f2f818c6036f465844c96c2151ee511db)), closes [#28](https://github.com/bedrock-python/grpc-client-kit/issues/28)
|
|
9
|
+
|
|
10
|
+
## [0.2.0](https://github.com/bedrock-python/grpc-client-kit/compare/grpc-client-kit-v0.1.2...grpc-client-kit-v0.2.0) (2026-09-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* settings models for the environment, mirroring the dataclasses ([edffb5f](https://github.com/bedrock-python/grpc-client-kit/commit/edffb5fdcf934cecd5788a99e37064133383d9cd))
|
|
16
|
+
|
|
3
17
|
## [0.1.2](https://github.com/bedrock-python/grpc-client-kit/compare/grpc-client-kit-v0.1.1...grpc-client-kit-v0.1.2) (2026-09-07)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: grpc-client-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Batteries-optional async gRPC client toolkit (channel pool, load balancing, health checking, streaming-aware interceptors, observability)
|
|
5
5
|
Project-URL: Repository, https://github.com/bedrock-python/grpc-client-kit
|
|
6
6
|
Project-URL: Documentation, https://bedrock-python.github.io/grpc-client-kit/
|
|
@@ -223,11 +223,15 @@ Requires-Python: >=3.12
|
|
|
223
223
|
Requires-Dist: grpcio<2.0.0,>=1.78.0
|
|
224
224
|
Provides-Extra: all
|
|
225
225
|
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'all'
|
|
226
|
+
Requires-Dist: dishka<2.0.0,>=1.4.0; extra == 'all'
|
|
226
227
|
Requires-Dist: grpcio-health-checking<2.0.0,>=1.78.0; extra == 'all'
|
|
227
228
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'all'
|
|
228
229
|
Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'all'
|
|
230
|
+
Requires-Dist: pydantic<3.0,>=2.0.0; extra == 'all'
|
|
229
231
|
Provides-Extra: deadline
|
|
230
232
|
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'deadline'
|
|
233
|
+
Provides-Extra: dishka
|
|
234
|
+
Requires-Dist: dishka<2.0.0,>=1.4.0; extra == 'dishka'
|
|
231
235
|
Provides-Extra: health
|
|
232
236
|
Requires-Dist: grpcio-health-checking<2.0.0,>=1.78.0; extra == 'health'
|
|
233
237
|
Provides-Extra: metrics
|
|
@@ -235,6 +239,8 @@ Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'metrics'
|
|
|
235
239
|
Provides-Extra: observability
|
|
236
240
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'observability'
|
|
237
241
|
Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'observability'
|
|
242
|
+
Provides-Extra: settings
|
|
243
|
+
Requires-Dist: pydantic<3.0,>=2.0.0; extra == 'settings'
|
|
238
244
|
Provides-Extra: tracing
|
|
239
245
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'tracing'
|
|
240
246
|
Description-Content-Type: text/markdown
|
|
@@ -307,6 +313,8 @@ pip install "grpc-client-kit[health]" # + grpc.health.v1 monitoring
|
|
|
307
313
|
pip install "grpc-client-kit[metrics,tracing]" # + Prometheus + OpenTelemetry
|
|
308
314
|
pip install "grpc-client-kit[observability]" # + metrics and tracing together
|
|
309
315
|
pip install "grpc-client-kit[deadline]" # + request budget propagation
|
|
316
|
+
pip install "grpc-client-kit[settings]" # + pydantic settings models
|
|
317
|
+
pip install "grpc-client-kit[dishka]" # + Dishka providers
|
|
310
318
|
pip install "grpc-client-kit[all]" # everything
|
|
311
319
|
```
|
|
312
320
|
|
|
@@ -362,7 +370,11 @@ Any object carrying the fields of `GrpcClientSettingsProtocol` works —
|
|
|
362
370
|
a pydantic model, a dataclass, a plain class. Everything optional
|
|
363
371
|
(`credentials`, `options`, `compression`, `metrics_registry`,
|
|
364
372
|
`sensitive_headers`) is read through `getattr`, so a minimal settings object
|
|
365
|
-
stays valid.
|
|
373
|
+
stays valid. Or take the shipped one: `grpc_client_kit.settings.BaseGrpcClientSettings`
|
|
374
|
+
(the `[settings]` extra) is that shape written down once — plain pydantic
|
|
375
|
+
models with the kit's own defaults and bounds, one per upstream, nested under
|
|
376
|
+
your service's `BaseSettings` for loading from the environment, and
|
|
377
|
+
`to_config()` on every section for a hand-built chain. See the
|
|
366
378
|
[configuration guide](https://bedrock-python.github.io/grpc-client-kit/guide/configuration/)
|
|
367
379
|
for the full field list.
|
|
368
380
|
|
|
@@ -511,12 +523,15 @@ names the extra to install.
|
|
|
511
523
|
| `channel.ChannelPool` | Channels pooled by full identity, idle eviction, health flags | core |
|
|
512
524
|
| `client.GrpcClient` | Stub factory: target selection + channel acquisition | core |
|
|
513
525
|
| `factory.GrpcClientFactory` | Settings → pool, balancer, health checker, per-target chains | core |
|
|
526
|
+
| `settings.BaseGrpcClientSettings` | The settings shape as pydantic models mirroring the dataclasses, `to_config()` on each | `settings` |
|
|
514
527
|
| `balancers.*` | Round-robin / random / weighted selection with health filtering | core |
|
|
515
528
|
| `interceptors.*` | Context, logging, timeout, wait-for-ready, retry, circuit breaker + chain builder | core |
|
|
516
529
|
| `deadline` + `interceptors.deadline` | The request budget in a contextvar, and the layer that trims every call to it | `deadline` |
|
|
517
530
|
| `interceptors.tracing` | OpenTelemetry CLIENT spans and `traceparent` injection | `tracing` |
|
|
518
531
|
| `interceptors.metrics` | Counters, latency histograms and an in-flight gauge | core¹ |
|
|
532
|
+
| `metrics.GrpcClientMetrics` | The Prometheus collector for them, shaped to join `grpc-server-kit`'s series | `metrics` |
|
|
519
533
|
| `health.HealthChecker` | Background `grpc.health.v1` probing with per-target backoff | `health` |
|
|
534
|
+
| `dishka.*` | Dishka providers owning the factory's lifecycle, one component per upstream | `dishka` |
|
|
520
535
|
| `protocols` / `validation` / `utils` | Settings seams, target validation, channel and metadata helpers | core |
|
|
521
536
|
|
|
522
537
|
¹ The metrics interceptor records through `GrpcClientMetricsProtocol` and works
|
|
@@ -528,10 +543,12 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
|
|
|
528
543
|
| :--- | :--- | :--- |
|
|
529
544
|
| `health` | `grpcio-health-checking` | `HealthChecker`, health-aware balancing and pooling |
|
|
530
545
|
| `tracing` | `opentelemetry-api` | `AsyncClientTracingInterceptor` (a pass-through without it) |
|
|
531
|
-
| `metrics` | `prometheus-client` | the
|
|
546
|
+
| `metrics` | `prometheus-client` | `GrpcClientMetrics`, the Prometheus collector; a custom registry needs no extra |
|
|
532
547
|
| `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
|
|
548
|
+
| `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
|
|
549
|
+
| `dishka` | `dishka` | `grpc_client_providers` and the three providers behind it |
|
|
533
550
|
| `observability` | `metrics` + `tracing` | both of the above |
|
|
534
|
-
| `all` | `deadline` + `health` + `metrics` + `tracing` | everything |
|
|
551
|
+
| `all` | `deadline` + `dishka` + `health` + `metrics` + `settings` + `tracing` | everything |
|
|
535
552
|
|
|
536
553
|
`deadline` is deliberately not part of `observability`: propagating a budget is
|
|
537
554
|
resilience, not telemetry, and an observability extra should not pull in a
|
|
@@ -565,7 +582,8 @@ Full documentation at [bedrock-python.github.io/grpc-client-kit](https://bedrock
|
|
|
565
582
|
| [Load balancing](https://bedrock-python.github.io/grpc-client-kit/guide/load-balancing/) | round-robin, random and weighted selection with health filtering |
|
|
566
583
|
| [Health](https://bedrock-python.github.io/grpc-client-kit/guide/health/) | `grpc.health.v1` probing, per-target backoff and cold starts |
|
|
567
584
|
| [Observability](https://bedrock-python.github.io/grpc-client-kit/guide/observability/) | log records, CLIENT spans and the metrics an RPC emits |
|
|
568
|
-
| [
|
|
585
|
+
| [Dependency injection](https://bedrock-python.github.io/grpc-client-kit/guide/dependency-injection/) | the Dishka providers, what they own, and one component per upstream |
|
|
586
|
+
| [Advanced](https://bedrock-python.github.io/grpc-client-kit/guide/advanced/) | interceptors that re-issue calls, target validation and ownership |
|
|
569
587
|
| [API reference](https://bedrock-python.github.io/grpc-client-kit/reference/) | generated from the source |
|
|
570
588
|
| [For AI agents](https://bedrock-python.github.io/grpc-client-kit/agents/) | the whole API surface, the rules that break code when broken and a map of the rest, on one page to hand to a coding assistant |
|
|
571
589
|
|
|
@@ -66,6 +66,8 @@ pip install "grpc-client-kit[health]" # + grpc.health.v1 monitoring
|
|
|
66
66
|
pip install "grpc-client-kit[metrics,tracing]" # + Prometheus + OpenTelemetry
|
|
67
67
|
pip install "grpc-client-kit[observability]" # + metrics and tracing together
|
|
68
68
|
pip install "grpc-client-kit[deadline]" # + request budget propagation
|
|
69
|
+
pip install "grpc-client-kit[settings]" # + pydantic settings models
|
|
70
|
+
pip install "grpc-client-kit[dishka]" # + Dishka providers
|
|
69
71
|
pip install "grpc-client-kit[all]" # everything
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -121,7 +123,11 @@ Any object carrying the fields of `GrpcClientSettingsProtocol` works —
|
|
|
121
123
|
a pydantic model, a dataclass, a plain class. Everything optional
|
|
122
124
|
(`credentials`, `options`, `compression`, `metrics_registry`,
|
|
123
125
|
`sensitive_headers`) is read through `getattr`, so a minimal settings object
|
|
124
|
-
stays valid.
|
|
126
|
+
stays valid. Or take the shipped one: `grpc_client_kit.settings.BaseGrpcClientSettings`
|
|
127
|
+
(the `[settings]` extra) is that shape written down once — plain pydantic
|
|
128
|
+
models with the kit's own defaults and bounds, one per upstream, nested under
|
|
129
|
+
your service's `BaseSettings` for loading from the environment, and
|
|
130
|
+
`to_config()` on every section for a hand-built chain. See the
|
|
125
131
|
[configuration guide](https://bedrock-python.github.io/grpc-client-kit/guide/configuration/)
|
|
126
132
|
for the full field list.
|
|
127
133
|
|
|
@@ -270,12 +276,15 @@ names the extra to install.
|
|
|
270
276
|
| `channel.ChannelPool` | Channels pooled by full identity, idle eviction, health flags | core |
|
|
271
277
|
| `client.GrpcClient` | Stub factory: target selection + channel acquisition | core |
|
|
272
278
|
| `factory.GrpcClientFactory` | Settings → pool, balancer, health checker, per-target chains | core |
|
|
279
|
+
| `settings.BaseGrpcClientSettings` | The settings shape as pydantic models mirroring the dataclasses, `to_config()` on each | `settings` |
|
|
273
280
|
| `balancers.*` | Round-robin / random / weighted selection with health filtering | core |
|
|
274
281
|
| `interceptors.*` | Context, logging, timeout, wait-for-ready, retry, circuit breaker + chain builder | core |
|
|
275
282
|
| `deadline` + `interceptors.deadline` | The request budget in a contextvar, and the layer that trims every call to it | `deadline` |
|
|
276
283
|
| `interceptors.tracing` | OpenTelemetry CLIENT spans and `traceparent` injection | `tracing` |
|
|
277
284
|
| `interceptors.metrics` | Counters, latency histograms and an in-flight gauge | core¹ |
|
|
285
|
+
| `metrics.GrpcClientMetrics` | The Prometheus collector for them, shaped to join `grpc-server-kit`'s series | `metrics` |
|
|
278
286
|
| `health.HealthChecker` | Background `grpc.health.v1` probing with per-target backoff | `health` |
|
|
287
|
+
| `dishka.*` | Dishka providers owning the factory's lifecycle, one component per upstream | `dishka` |
|
|
279
288
|
| `protocols` / `validation` / `utils` | Settings seams, target validation, channel and metadata helpers | core |
|
|
280
289
|
|
|
281
290
|
¹ The metrics interceptor records through `GrpcClientMetricsProtocol` and works
|
|
@@ -287,10 +296,12 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
|
|
|
287
296
|
| :--- | :--- | :--- |
|
|
288
297
|
| `health` | `grpcio-health-checking` | `HealthChecker`, health-aware balancing and pooling |
|
|
289
298
|
| `tracing` | `opentelemetry-api` | `AsyncClientTracingInterceptor` (a pass-through without it) |
|
|
290
|
-
| `metrics` | `prometheus-client` | the
|
|
299
|
+
| `metrics` | `prometheus-client` | `GrpcClientMetrics`, the Prometheus collector; a custom registry needs no extra |
|
|
291
300
|
| `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
|
|
301
|
+
| `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
|
|
302
|
+
| `dishka` | `dishka` | `grpc_client_providers` and the three providers behind it |
|
|
292
303
|
| `observability` | `metrics` + `tracing` | both of the above |
|
|
293
|
-
| `all` | `deadline` + `health` + `metrics` + `tracing` | everything |
|
|
304
|
+
| `all` | `deadline` + `dishka` + `health` + `metrics` + `settings` + `tracing` | everything |
|
|
294
305
|
|
|
295
306
|
`deadline` is deliberately not part of `observability`: propagating a budget is
|
|
296
307
|
resilience, not telemetry, and an observability extra should not pull in a
|
|
@@ -324,7 +335,8 @@ Full documentation at [bedrock-python.github.io/grpc-client-kit](https://bedrock
|
|
|
324
335
|
| [Load balancing](https://bedrock-python.github.io/grpc-client-kit/guide/load-balancing/) | round-robin, random and weighted selection with health filtering |
|
|
325
336
|
| [Health](https://bedrock-python.github.io/grpc-client-kit/guide/health/) | `grpc.health.v1` probing, per-target backoff and cold starts |
|
|
326
337
|
| [Observability](https://bedrock-python.github.io/grpc-client-kit/guide/observability/) | log records, CLIENT spans and the metrics an RPC emits |
|
|
327
|
-
| [
|
|
338
|
+
| [Dependency injection](https://bedrock-python.github.io/grpc-client-kit/guide/dependency-injection/) | the Dishka providers, what they own, and one component per upstream |
|
|
339
|
+
| [Advanced](https://bedrock-python.github.io/grpc-client-kit/guide/advanced/) | interceptors that re-issue calls, target validation and ownership |
|
|
328
340
|
| [API reference](https://bedrock-python.github.io/grpc-client-kit/reference/) | generated from the source |
|
|
329
341
|
| [For AI agents](https://bedrock-python.github.io/grpc-client-kit/agents/) | the whole API surface, the rules that break code when broken and a map of the rest, on one page to hand to a coding assistant |
|
|
330
342
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0" # x-release-please-version
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Dishka providers owning the client lifecycle (``[dishka]`` extra).
|
|
2
|
+
|
|
3
|
+
What an application otherwise writes by hand — build the factory from the settings, start the health
|
|
4
|
+
checker with the container, close the pool with it — lives here as three providers, one per thing a
|
|
5
|
+
container hands out, and a bundle registering all three::
|
|
6
|
+
|
|
7
|
+
from dishka import make_async_container
|
|
8
|
+
|
|
9
|
+
from grpc_client_kit import GrpcClientFactory
|
|
10
|
+
from grpc_client_kit.dishka import grpc_client_providers
|
|
11
|
+
|
|
12
|
+
container = make_async_container(*grpc_client_providers(settings.users_grpc))
|
|
13
|
+
factory = await container.get(GrpcClientFactory)
|
|
14
|
+
users = factory.create_client(UserStub)
|
|
15
|
+
...
|
|
16
|
+
await container.close() # stops the checker, closes the pool
|
|
17
|
+
|
|
18
|
+
- `GrpcClientSettingsProvider` holds the settings object and provides it as
|
|
19
|
+
`protocols.GrpcClientSettingsProtocol`, so `settings.BaseGrpcClientSettings` or anything structural
|
|
20
|
+
works.
|
|
21
|
+
- `PrometheusGrpcClientMetricsProvider` provides ``GrpcClientMetricsProtocol | None``: the cached
|
|
22
|
+
`metrics.GrpcClientMetrics` when ``settings.metrics_enabled``, ``None`` otherwise — ``None`` being
|
|
23
|
+
the kit's own spelling of "no metrics", which the factory leaves the layer out for.
|
|
24
|
+
- `AsyncGrpcClientProvider` provides `factory.GrpcClientFactory` in APP scope from an async generator,
|
|
25
|
+
entered when first resolved and left when the container closes.
|
|
26
|
+
|
|
27
|
+
Several upstreams in one container are Dishka components: every provider takes ``component=``, so
|
|
28
|
+
one bundle per upstream, each in its own component, is one factory per upstream::
|
|
29
|
+
|
|
30
|
+
container = make_async_container(
|
|
31
|
+
*grpc_client_providers(settings.users_grpc, component="users"),
|
|
32
|
+
*grpc_client_providers(settings.orders_grpc, component="orders"),
|
|
33
|
+
)
|
|
34
|
+
users = await container.get(GrpcClientFactory, component="users")
|
|
35
|
+
|
|
36
|
+
Dishka resolves a component's dependencies inside that component, which is why each one carries its
|
|
37
|
+
own three providers; the collector they hand out is the same instance, cached per prefix, and its
|
|
38
|
+
series are told apart by ``service``. A single upstream uses the default component and needs none of
|
|
39
|
+
this.
|
|
40
|
+
|
|
41
|
+
This module needs the ``dishka`` extra (``grpc-client-kit[dishka]``); importing it without raises an
|
|
42
|
+
``ImportError`` naming the extra.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import logging
|
|
48
|
+
from collections.abc import AsyncIterator
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from dishka import Provider, Scope, provide
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
raise ImportError("Install grpc-client-kit[dishka] (dishka) to use the Dishka providers") from exc
|
|
54
|
+
|
|
55
|
+
from .factory import GrpcClientFactory
|
|
56
|
+
from .interceptors.metrics import HAS_METRICS
|
|
57
|
+
from .protocols import GrpcClientMetricsProtocol, GrpcClientSettingsProtocol
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GrpcClientSettingsProvider(Provider):
|
|
63
|
+
"""Provide one upstream's settings object as ``GrpcClientSettingsProtocol``.
|
|
64
|
+
|
|
65
|
+
Construct it with anything structurally satisfying the protocol — `settings.BaseGrpcClientSettings`,
|
|
66
|
+
a dataclass, a plain class. The other providers request the settings through the protocol, which
|
|
67
|
+
is what lets a second upstream be a second component rather than a second container.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
scope = Scope.APP
|
|
71
|
+
|
|
72
|
+
def __init__(self, settings: GrpcClientSettingsProtocol, *, component: str | None = None) -> None:
|
|
73
|
+
super().__init__(component=component)
|
|
74
|
+
self._settings = settings
|
|
75
|
+
|
|
76
|
+
@provide
|
|
77
|
+
def settings(self) -> GrpcClientSettingsProtocol:
|
|
78
|
+
"""Provide the settings object as it was given."""
|
|
79
|
+
return self._settings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PrometheusGrpcClientMetricsProvider(Provider):
|
|
83
|
+
"""Provide the registry the factory records into: `metrics.GrpcClientMetrics`, or ``None``.
|
|
84
|
+
|
|
85
|
+
``None`` when ``settings.metrics_enabled`` is off, and ``None`` with a warning when it is on but
|
|
86
|
+
the ``[metrics]`` extra is absent — the degradation the chain builder applies, so the provider can
|
|
87
|
+
always be registered. The collector comes from `metrics.get_grpc_client_metrics`, one instance per
|
|
88
|
+
prefix on the default registry, so a container rebuilt per test never asks Prometheus to register
|
|
89
|
+
the same series twice.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
scope = Scope.APP
|
|
93
|
+
|
|
94
|
+
def __init__(self, *, prefix: str | None = None, component: str | None = None) -> None:
|
|
95
|
+
super().__init__(component=component)
|
|
96
|
+
self._prefix = prefix
|
|
97
|
+
|
|
98
|
+
@provide
|
|
99
|
+
def metrics(self, settings: GrpcClientSettingsProtocol) -> GrpcClientMetricsProtocol | None:
|
|
100
|
+
"""Provide the collector when metrics are enabled and the extra is installed, ``None`` otherwise."""
|
|
101
|
+
if not settings.metrics_enabled:
|
|
102
|
+
return None
|
|
103
|
+
if not HAS_METRICS:
|
|
104
|
+
logger.warning("Metrics requested but grpc-client-kit[metrics] is not installed; metrics are disabled")
|
|
105
|
+
return None
|
|
106
|
+
from .metrics import get_grpc_client_metrics # noqa: PLC0415 - lazy: needs the [metrics] extra
|
|
107
|
+
|
|
108
|
+
return get_grpc_client_metrics(self._prefix)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AsyncGrpcClientProvider(Provider):
|
|
112
|
+
"""Provide `factory.GrpcClientFactory` in APP scope, entered on first use and closed with the container.
|
|
113
|
+
|
|
114
|
+
The factory builds and owns the pool and the health checker: resolving it starts the checker and
|
|
115
|
+
waits for its first pass (``ready_timeout``); closing the container stops the checker and closes
|
|
116
|
+
the pool, giving in-flight RPCs ``shutdown_grace`` — exactly what ``async with factory`` does.
|
|
117
|
+
Clients come from the factory as usual, ``factory.create_client(UserStub)``, and are cheap enough
|
|
118
|
+
to be request-scoped.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
scope = Scope.APP
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
shutdown_grace: float | None = 5.0,
|
|
127
|
+
ready_timeout: float | None = 10.0,
|
|
128
|
+
component: str | None = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
super().__init__(component=component)
|
|
131
|
+
self._shutdown_grace = shutdown_grace
|
|
132
|
+
self._ready_timeout = ready_timeout
|
|
133
|
+
|
|
134
|
+
@provide
|
|
135
|
+
async def factory(
|
|
136
|
+
self,
|
|
137
|
+
settings: GrpcClientSettingsProtocol,
|
|
138
|
+
metrics: GrpcClientMetricsProtocol | None,
|
|
139
|
+
) -> AsyncIterator[GrpcClientFactory]:
|
|
140
|
+
"""Build the factory from the settings and the registry, and close it with the container."""
|
|
141
|
+
async with GrpcClientFactory(
|
|
142
|
+
settings=settings,
|
|
143
|
+
shutdown_grace=self._shutdown_grace,
|
|
144
|
+
ready_timeout=self._ready_timeout,
|
|
145
|
+
metrics=metrics,
|
|
146
|
+
) as factory:
|
|
147
|
+
yield factory
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def grpc_client_providers(
|
|
151
|
+
settings: GrpcClientSettingsProtocol,
|
|
152
|
+
*,
|
|
153
|
+
component: str | None = None,
|
|
154
|
+
metrics_prefix: str | None = None,
|
|
155
|
+
shutdown_grace: float | None = 5.0,
|
|
156
|
+
ready_timeout: float | None = 10.0,
|
|
157
|
+
) -> tuple[Provider, ...]:
|
|
158
|
+
"""Return the three providers of one upstream, for one-line registration.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
settings: The upstream's settings object; anything satisfying `protocols.GrpcClientSettingsProtocol`.
|
|
162
|
+
component: The Dishka component to register the providers in — one per upstream when a
|
|
163
|
+
container serves several, the default component otherwise.
|
|
164
|
+
metrics_prefix: Prefix for the Prometheus metric names.
|
|
165
|
+
shutdown_grace: Seconds in-flight RPCs get to finish when the container closes.
|
|
166
|
+
ready_timeout: Seconds the factory waits for the first health check pass when resolved.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The providers, to unpack into ``make_async_container``.
|
|
170
|
+
"""
|
|
171
|
+
return (
|
|
172
|
+
GrpcClientSettingsProvider(settings, component=component),
|
|
173
|
+
PrometheusGrpcClientMetricsProvider(prefix=metrics_prefix, component=component),
|
|
174
|
+
AsyncGrpcClientProvider(shutdown_grace=shutdown_grace, ready_timeout=ready_timeout, component=component),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = [
|
|
179
|
+
"AsyncGrpcClientProvider",
|
|
180
|
+
"GrpcClientSettingsProvider",
|
|
181
|
+
"PrometheusGrpcClientMetricsProvider",
|
|
182
|
+
"grpc_client_providers",
|
|
183
|
+
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from dataclasses import fields
|
|
4
5
|
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
6
7
|
import grpc.aio
|
|
@@ -8,7 +9,7 @@ import grpc.aio
|
|
|
8
9
|
from .balancers import LoadBalancer, LoadBalancerConfig, LoadBalancingStrategy, create_balancer
|
|
9
10
|
from .channel import ChannelPool
|
|
10
11
|
from .client import GrpcClient
|
|
11
|
-
from .config import GrpcClientConfig
|
|
12
|
+
from .config import ConnectivityConfig, GrpcClientConfig
|
|
12
13
|
from .interceptors import (
|
|
13
14
|
CircuitBreakerConfig,
|
|
14
15
|
DeadlineBudgetConfig,
|
|
@@ -35,6 +36,10 @@ logger = logging.getLogger(__name__)
|
|
|
35
36
|
|
|
36
37
|
_MISSING_MESSAGE = "Install grpc-client-kit[health] (grpcio-health-checking) to use gRPC health checking"
|
|
37
38
|
|
|
39
|
+
# What a duck-typed `connectivity` block may carry: the dataclass's own fields, so a knob added there
|
|
40
|
+
# is read from settings without this module having to hear about it.
|
|
41
|
+
_CONNECTIVITY_FIELDS = tuple(field.name for field in fields(ConnectivityConfig))
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
def _validate_settings(settings: object | None) -> None:
|
|
40
45
|
"""Refuse a settings object that cannot satisfy the factory, while the mistake is visible.
|
|
@@ -93,6 +98,7 @@ class GrpcClientFactory:
|
|
|
93
98
|
pool: ChannelProviderProtocol | None = None,
|
|
94
99
|
shutdown_grace: float | None = 5.0,
|
|
95
100
|
ready_timeout: float | None = 10.0,
|
|
101
|
+
metrics: GrpcClientMetricsProtocol | None = None,
|
|
96
102
|
) -> None:
|
|
97
103
|
"""Initialize the factory.
|
|
98
104
|
|
|
@@ -113,6 +119,12 @@ class GrpcClientFactory:
|
|
|
113
119
|
that pass every target reads unhealthy, so entering the context without waiting
|
|
114
120
|
would make the first call of every freshly started pod fail deterministically.
|
|
115
121
|
``None`` waits indefinitely; only relevant when health checking is configured.
|
|
122
|
+
metrics: The registry RPC metrics, retries, breaker state and pool statistics are
|
|
123
|
+
recorded into: the default for every client this factory creates, and the one
|
|
124
|
+
the pool it creates reports to. Wins over ``settings.metrics_registry``;
|
|
125
|
+
`create_client(metrics=...)` wins over it per client. The shipped settings
|
|
126
|
+
models carry no registry, a registry being a runtime object, so this is where
|
|
127
|
+
one meets them.
|
|
116
128
|
|
|
117
129
|
Raises:
|
|
118
130
|
TypeError: If `settings` does not satisfy `GrpcClientSettingsProtocol`. Validated
|
|
@@ -130,21 +142,24 @@ class GrpcClientFactory:
|
|
|
130
142
|
# reuse the same interceptor instances, or every create_client would mint a fresh channel
|
|
131
143
|
# identity and a per-request client pattern would open a connection per request.
|
|
132
144
|
self._chains: dict[tuple[str, str], list[grpc.aio.ClientInterceptor]] = {}
|
|
145
|
+
# The registry every client and the owned pool default to: the explicit one, else the
|
|
146
|
+
# settings' own — the shipped settings models carry none, a registry being a runtime object.
|
|
147
|
+
settings_registry = getattr(settings, "metrics_registry", None) if settings else None
|
|
148
|
+
self._metrics: GrpcClientMetricsProtocol | None = metrics if metrics is not None else settings_registry
|
|
133
149
|
|
|
134
150
|
if pool:
|
|
135
151
|
self._pool = pool
|
|
136
152
|
else:
|
|
137
153
|
self._owns_pool = True
|
|
138
|
-
metrics = getattr(settings, "metrics_registry", None) if settings else None
|
|
139
154
|
|
|
140
155
|
if settings and settings.pool:
|
|
141
156
|
self._pool = ChannelPool(
|
|
142
157
|
max_channels_per_target=settings.pool.max_channels_per_target,
|
|
143
158
|
idle_timeout=settings.pool.idle_timeout,
|
|
144
|
-
metrics=
|
|
159
|
+
metrics=self._metrics,
|
|
145
160
|
)
|
|
146
161
|
else:
|
|
147
|
-
self._pool = ChannelPool(metrics=
|
|
162
|
+
self._pool = ChannelPool(metrics=self._metrics)
|
|
148
163
|
|
|
149
164
|
# Build and start HealthChecker if configured and we have targets
|
|
150
165
|
if settings and settings.health_checker and settings.targets:
|
|
@@ -247,7 +262,8 @@ class GrpcClientFactory:
|
|
|
247
262
|
stub_class: The gRPC stub class to instantiate.
|
|
248
263
|
target: Optional target override. If not provided, uses target from settings.
|
|
249
264
|
service_name: Name of the service for observability. Defaults to stub class name.
|
|
250
|
-
metrics: Optional metrics registry, overriding
|
|
265
|
+
metrics: Optional metrics registry for this client alone, overriding the factory's
|
|
266
|
+
``metrics`` and `settings.metrics_registry`.
|
|
251
267
|
interceptors: Optional list of additional custom interceptors. These instances are
|
|
252
268
|
shared by every target, unlike the chain the factory builds around them.
|
|
253
269
|
|
|
@@ -306,6 +322,10 @@ class GrpcClientFactory:
|
|
|
306
322
|
metrics_registry=metrics_registry,
|
|
307
323
|
service_name=actual_service_name,
|
|
308
324
|
sensitive_headers=getattr(self._settings, "sensitive_headers", None),
|
|
325
|
+
sensitive_methods=getattr(self._settings, "sensitive_methods", None),
|
|
326
|
+
sensitive_patterns=getattr(self._settings, "sensitive_patterns", None),
|
|
327
|
+
log_request_payload=getattr(self._settings, "log_request_payload", False),
|
|
328
|
+
log_response_payload=getattr(self._settings, "log_response_payload", False),
|
|
309
329
|
enable_method_label=getattr(self._settings, "enable_method_label", True),
|
|
310
330
|
success_log_level=getattr(self._settings, "success_log_level", logging.INFO),
|
|
311
331
|
)
|
|
@@ -344,8 +364,9 @@ class GrpcClientFactory:
|
|
|
344
364
|
def _resolve_metrics_registry(self, metrics: GrpcClientMetricsProtocol | None) -> GrpcClientMetricsProtocol | None:
|
|
345
365
|
"""Pick the registry RPC metrics are recorded into.
|
|
346
366
|
|
|
347
|
-
An explicit argument wins over
|
|
348
|
-
|
|
367
|
+
An explicit argument wins over the factory's default — its own ``metrics`` argument, else
|
|
368
|
+
`settings.metrics_registry`, which otherwise only fed pool statistics and left
|
|
369
|
+
`metrics_enabled` clients recording nothing at all.
|
|
349
370
|
|
|
350
371
|
Args:
|
|
351
372
|
metrics: The registry passed to `create_client`, if any.
|
|
@@ -353,13 +374,12 @@ class GrpcClientFactory:
|
|
|
353
374
|
Returns:
|
|
354
375
|
The registry to use, or None if metrics cannot be recorded.
|
|
355
376
|
"""
|
|
356
|
-
|
|
357
|
-
registry: GrpcClientMetricsProtocol | None = metrics if metrics is not None else settings_registry
|
|
377
|
+
registry: GrpcClientMetricsProtocol | None = metrics if metrics is not None else self._metrics
|
|
358
378
|
|
|
359
379
|
if registry is None and self._settings and self._settings.metrics_enabled:
|
|
360
380
|
logger.warning(
|
|
361
381
|
"metrics_enabled is set but no metrics registry is available. "
|
|
362
|
-
"Pass 'metrics=' to create_client() or set 'metrics_registry' in settings; "
|
|
382
|
+
"Pass 'metrics=' to the factory or to create_client(), or set 'metrics_registry' in settings; "
|
|
363
383
|
"gRPC client metrics are disabled."
|
|
364
384
|
)
|
|
365
385
|
|
|
@@ -371,8 +391,8 @@ class GrpcClientFactory:
|
|
|
371
391
|
Channel construction settings are optional blocks (see `protocols.GrpcChannelExtrasProtocol`)
|
|
372
392
|
and are read with `getattr`, so a settings model that declares none of them still satisfies
|
|
373
393
|
the factory. ``connectivity`` joins them on the same terms: a settings object that carries a
|
|
374
|
-
`config.ConnectivityConfig`
|
|
375
|
-
not is left with gRPC's defaults.
|
|
394
|
+
`config.ConnectivityConfig` — or a block with its fields — under that name has its channels
|
|
395
|
+
tuned by it, and one that does not is left with gRPC's defaults.
|
|
376
396
|
"""
|
|
377
397
|
if self._settings:
|
|
378
398
|
actual_target = target or self._settings.target
|
|
@@ -380,7 +400,7 @@ class GrpcClientFactory:
|
|
|
380
400
|
credentials = getattr(self._settings, "credentials", None)
|
|
381
401
|
options = getattr(self._settings, "options", None)
|
|
382
402
|
compression = getattr(self._settings, "compression", None)
|
|
383
|
-
connectivity = getattr(self._settings, "connectivity", None)
|
|
403
|
+
connectivity = self._build_connectivity_config(getattr(self._settings, "connectivity", None))
|
|
384
404
|
else:
|
|
385
405
|
actual_target = target
|
|
386
406
|
insecure = False
|
|
@@ -422,11 +442,23 @@ class GrpcClientFactory:
|
|
|
422
442
|
health_checker=self._health_checker,
|
|
423
443
|
)
|
|
424
444
|
|
|
445
|
+
def _build_connectivity_config(self, s: Any | None) -> ConnectivityConfig | None:
|
|
446
|
+
"""Build ConnectivityConfig from an optional settings block (duck-typed).
|
|
447
|
+
|
|
448
|
+
A ready `config.ConnectivityConfig` is taken as it is. Any other block is read field by
|
|
449
|
+
field, and a field it does not carry keeps the dataclass's own default.
|
|
450
|
+
"""
|
|
451
|
+
if not s:
|
|
452
|
+
return None
|
|
453
|
+
if isinstance(s, ConnectivityConfig):
|
|
454
|
+
return s
|
|
455
|
+
return ConnectivityConfig(**{name: getattr(s, name) for name in _CONNECTIVITY_FIELDS if hasattr(s, name)})
|
|
456
|
+
|
|
425
457
|
def _build_timeout_config(self, s: TimeoutSettingsProtocol | None) -> TimeoutConfig | None:
|
|
426
|
-
"""Build TimeoutConfig from settings protocol."""
|
|
458
|
+
"""Build TimeoutConfig from settings protocol; per-method budgets are read when the block has them."""
|
|
427
459
|
if not s:
|
|
428
460
|
return None
|
|
429
|
-
return TimeoutConfig(default=s.default)
|
|
461
|
+
return TimeoutConfig(default=s.default, per_method=dict(getattr(s, "per_method", None) or {}))
|
|
430
462
|
|
|
431
463
|
def _build_retry_config(
|
|
432
464
|
self, s: RetrySettingsProtocol | None, metrics: GrpcClientMetricsProtocol | None = None
|