grpc-client-kit 0.3.0__tar.gz → 0.4.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 (36) hide show
  1. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/CHANGELOG.md +7 -0
  2. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/PKG-INFO +4 -4
  3. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/README.md +3 -3
  4. grpc_client_kit-0.4.0/grpc_client_kit/__version__.py +1 -0
  5. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/dishka.py +101 -7
  6. grpc_client_kit-0.3.0/grpc_client_kit/__version__.py +0 -1
  7. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/.gitignore +0 -0
  8. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/LICENSE +0 -0
  9. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/__init__.py +0 -0
  10. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/balancers.py +0 -0
  11. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/channel.py +0 -0
  12. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/client.py +0 -0
  13. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/config.py +0 -0
  14. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/deadline.py +0 -0
  15. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/errors.py +0 -0
  16. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/factory.py +0 -0
  17. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/health.py +0 -0
  18. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/__init__.py +0 -0
  19. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/base.py +0 -0
  20. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/circuit_breaker.py +0 -0
  21. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/client_logging.py +0 -0
  22. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/context.py +0 -0
  23. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/deadline.py +0 -0
  24. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/metrics.py +0 -0
  25. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/outlier.py +0 -0
  26. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/retry.py +0 -0
  27. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/timeout.py +0 -0
  28. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/tracing.py +0 -0
  29. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/interceptors/wait_for_ready.py +0 -0
  30. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/metrics.py +0 -0
  31. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/protocols.py +0 -0
  32. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/py.typed +0 -0
  33. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/settings.py +0 -0
  34. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/utils.py +0 -0
  35. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/grpc_client_kit/validation.py +0 -0
  36. {grpc_client_kit-0.3.0 → grpc_client_kit-0.4.0}/pyproject.toml +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/bedrock-python/grpc-client-kit/compare/grpc-client-kit-v0.3.0...grpc-client-kit-v0.4.0) (2026-09-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * a container-owned channel pool for the Dishka providers, shared by every upstream told to borrow it ([ecdc4e2](https://github.com/bedrock-python/grpc-client-kit/commit/ecdc4e264bdad2b80311f25bfd922ad15ea2db07)), closes [#31](https://github.com/bedrock-python/grpc-client-kit/issues/31)
9
+
3
10
  ## [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
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: grpc-client-kit
3
- Version: 0.3.0
3
+ Version: 0.4.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/
@@ -531,7 +531,7 @@ names the extra to install.
531
531
  | `interceptors.metrics` | Counters, latency histograms and an in-flight gauge | core¹ |
532
532
  | `metrics.GrpcClientMetrics` | The Prometheus collector for them, shaped to join `grpc-server-kit`'s series | `metrics` |
533
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` |
534
+ | `dishka.*` | Dishka providers owning the factory's lifecycle, one component per upstream, one pool shared by several | `dishka` |
535
535
  | `protocols` / `validation` / `utils` | Settings seams, target validation, channel and metadata helpers | core |
536
536
 
537
537
  ¹ The metrics interceptor records through `GrpcClientMetricsProtocol` and works
@@ -546,7 +546,7 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
546
546
  | `metrics` | `prometheus-client` | `GrpcClientMetrics`, the Prometheus collector; a custom registry needs no extra |
547
547
  | `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
548
548
  | `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
549
- | `dishka` | `dishka` | `grpc_client_providers` and the three providers behind it |
549
+ | `dishka` | `dishka` | `grpc_client_providers`, the three providers behind it, and the pool provider several upstreams share |
550
550
  | `observability` | `metrics` + `tracing` | both of the above |
551
551
  | `all` | `deadline` + `dishka` + `health` + `metrics` + `settings` + `tracing` | everything |
552
552
 
@@ -582,7 +582,7 @@ Full documentation at [bedrock-python.github.io/grpc-client-kit](https://bedrock
582
582
  | [Load balancing](https://bedrock-python.github.io/grpc-client-kit/guide/load-balancing/) | round-robin, random and weighted selection with health filtering |
583
583
  | [Health](https://bedrock-python.github.io/grpc-client-kit/guide/health/) | `grpc.health.v1` probing, per-target backoff and cold starts |
584
584
  | [Observability](https://bedrock-python.github.io/grpc-client-kit/guide/observability/) | log records, CLIENT spans and the metrics an RPC emits |
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 |
585
+ | [Dependency injection](https://bedrock-python.github.io/grpc-client-kit/guide/dependency-injection/) | the Dishka providers, what they own, one component per upstream, one pool shared by several |
586
586
  | [Advanced](https://bedrock-python.github.io/grpc-client-kit/guide/advanced/) | interceptors that re-issue calls, target validation and ownership |
587
587
  | [API reference](https://bedrock-python.github.io/grpc-client-kit/reference/) | generated from the source |
588
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 |
@@ -284,7 +284,7 @@ names the extra to install.
284
284
  | `interceptors.metrics` | Counters, latency histograms and an in-flight gauge | core¹ |
285
285
  | `metrics.GrpcClientMetrics` | The Prometheus collector for them, shaped to join `grpc-server-kit`'s series | `metrics` |
286
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` |
287
+ | `dishka.*` | Dishka providers owning the factory's lifecycle, one component per upstream, one pool shared by several | `dishka` |
288
288
  | `protocols` / `validation` / `utils` | Settings seams, target validation, channel and metadata helpers | core |
289
289
 
290
290
  ¹ The metrics interceptor records through `GrpcClientMetricsProtocol` and works
@@ -299,7 +299,7 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
299
299
  | `metrics` | `prometheus-client` | `GrpcClientMetrics`, the Prometheus collector; a custom registry needs no extra |
300
300
  | `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
301
301
  | `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
302
- | `dishka` | `dishka` | `grpc_client_providers` and the three providers behind it |
302
+ | `dishka` | `dishka` | `grpc_client_providers`, the three providers behind it, and the pool provider several upstreams share |
303
303
  | `observability` | `metrics` + `tracing` | both of the above |
304
304
  | `all` | `deadline` + `dishka` + `health` + `metrics` + `settings` + `tracing` | everything |
305
305
 
@@ -335,7 +335,7 @@ Full documentation at [bedrock-python.github.io/grpc-client-kit](https://bedrock
335
335
  | [Load balancing](https://bedrock-python.github.io/grpc-client-kit/guide/load-balancing/) | round-robin, random and weighted selection with health filtering |
336
336
  | [Health](https://bedrock-python.github.io/grpc-client-kit/guide/health/) | `grpc.health.v1` probing, per-target backoff and cold starts |
337
337
  | [Observability](https://bedrock-python.github.io/grpc-client-kit/guide/observability/) | log records, CLIENT spans and the metrics an RPC emits |
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 |
338
+ | [Dependency injection](https://bedrock-python.github.io/grpc-client-kit/guide/dependency-injection/) | the Dishka providers, what they own, one component per upstream, one pool shared by several |
339
339
  | [Advanced](https://bedrock-python.github.io/grpc-client-kit/guide/advanced/) | interceptors that re-issue calls, target validation and ownership |
340
340
  | [API reference](https://bedrock-python.github.io/grpc-client-kit/reference/) | generated from the source |
341
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 |
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0" # x-release-please-version
@@ -1,8 +1,8 @@
1
1
  """Dishka providers owning the client lifecycle (``[dishka]`` extra).
2
2
 
3
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::
4
+ checker with the container, close the pool with it — lives here as providers, one per thing a
5
+ container hands out, and a bundle registering the three of one upstream::
6
6
 
7
7
  from dishka import make_async_container
8
8
 
@@ -23,6 +23,8 @@ container hands out, and a bundle registering all three::
23
23
  the kit's own spelling of "no metrics", which the factory leaves the layer out for.
24
24
  - `AsyncGrpcClientProvider` provides `factory.GrpcClientFactory` in APP scope from an async generator,
25
25
  entered when first resolved and left when the container closes.
26
+ - `AsyncChannelPoolProvider` provides `channel.ChannelPool` the same way, for the upstreams that share
27
+ one; a factory with ``shared_pool=True`` borrows it instead of building its own.
26
28
 
27
29
  Several upstreams in one container are Dishka components: every provider takes ``component=``, so
28
30
  one bundle per upstream, each in its own component, is one factory per upstream::
@@ -38,6 +40,16 @@ own three providers; the collector they hand out is the same instance, cached pe
38
40
  series are told apart by ``service``. A single upstream uses the default component and needs none of
39
41
  this.
40
42
 
43
+ One pool for all of them is `AsyncChannelPoolProvider` in the default component and ``shared_pool=True``
44
+ on every bundle: each factory then borrows that `channel.ChannelPool` instead of building its own, and
45
+ the container closes it once, after the factories, with the pool provider's grace::
46
+
47
+ container = make_async_container(
48
+ AsyncChannelPoolProvider(settings.grpc_pool, metrics=get_grpc_client_metrics()),
49
+ *grpc_client_providers(settings.users_grpc, component="users", shared_pool=True),
50
+ *grpc_client_providers(settings.orders_grpc, component="orders", shared_pool=True),
51
+ )
52
+
41
53
  This module needs the ``dishka`` extra (``grpc-client-kit[dishka]``); importing it without raises an
42
54
  ``ImportError`` naming the extra.
43
55
  """
@@ -46,15 +58,17 @@ from __future__ import annotations
46
58
 
47
59
  import logging
48
60
  from collections.abc import AsyncIterator
61
+ from typing import Annotated
49
62
 
50
63
  try:
51
- from dishka import Provider, Scope, provide
64
+ from dishka import DEFAULT_COMPONENT, FromComponent, Provider, Scope, provide
52
65
  except ImportError as exc:
53
66
  raise ImportError("Install grpc-client-kit[dishka] (dishka) to use the Dishka providers") from exc
54
67
 
68
+ from .channel import ChannelPool
55
69
  from .factory import GrpcClientFactory
56
70
  from .interceptors.metrics import HAS_METRICS
57
- from .protocols import GrpcClientMetricsProtocol, GrpcClientSettingsProtocol
71
+ from .protocols import ChannelPoolSettingsProtocol, GrpcClientMetricsProtocol, GrpcClientSettingsProtocol
58
72
 
59
73
  logger = logging.getLogger(__name__)
60
74
 
@@ -108,6 +122,54 @@ class PrometheusGrpcClientMetricsProvider(Provider):
108
122
  return get_grpc_client_metrics(self._prefix)
109
123
 
110
124
 
125
+ class AsyncChannelPoolProvider(Provider):
126
+ """Provide one `channel.ChannelPool` in APP scope, built on first use and drained with the container.
127
+
128
+ The pool several upstreams share: registered in the default component, it is what every
129
+ `AsyncGrpcClientProvider` with ``shared_pool=True`` borrows, and the container closes it once —
130
+ ``close_all(grace=shutdown_grace)``, after the factories that used it have left. Sized from a
131
+ `protocols.ChannelPoolSettingsProtocol` — `settings.BaseChannelPoolSettings` nested at the top of
132
+ the service's settings is one deployment-wide section — and at the pool's own defaults without one.
133
+
134
+ ``metrics`` is the registry the pool statistics are recorded into. It is handed in here, the way
135
+ `factory.GrpcClientFactory` takes it, rather than requested from the container: a container with
136
+ one component per upstream has a registry per component and none in the default one, so there is
137
+ no single seam for the pool to ask. `metrics.get_grpc_client_metrics` returns the instance the
138
+ bundles hand out.
139
+ """
140
+
141
+ scope = Scope.APP
142
+
143
+ def __init__(
144
+ self,
145
+ settings: ChannelPoolSettingsProtocol | None = None,
146
+ *,
147
+ metrics: GrpcClientMetricsProtocol | None = None,
148
+ shutdown_grace: float | None = 5.0,
149
+ component: str | None = None,
150
+ ) -> None:
151
+ super().__init__(component=component)
152
+ self._settings = settings
153
+ self._metrics = metrics
154
+ self._shutdown_grace = shutdown_grace
155
+
156
+ @provide
157
+ async def pool(self) -> AsyncIterator[ChannelPool]:
158
+ """Build the pool from the settings and the registry, and drain it with the container."""
159
+ if self._settings is not None:
160
+ pool = ChannelPool(
161
+ max_channels_per_target=self._settings.max_channels_per_target,
162
+ idle_timeout=self._settings.idle_timeout,
163
+ metrics=self._metrics,
164
+ )
165
+ else:
166
+ pool = ChannelPool(metrics=self._metrics)
167
+ try:
168
+ yield pool
169
+ finally:
170
+ await pool.close_all(grace=self._shutdown_grace)
171
+
172
+
111
173
  class AsyncGrpcClientProvider(Provider):
112
174
  """Provide `factory.GrpcClientFactory` in APP scope, entered on first use and closed with the container.
113
175
 
@@ -116,6 +178,13 @@ class AsyncGrpcClientProvider(Provider):
116
178
  the pool, giving in-flight RPCs ``shutdown_grace`` — exactly what ``async with factory`` does.
117
179
  Clients come from the factory as usual, ``factory.create_client(UserStub)``, and are cheap enough
118
180
  to be request-scoped.
181
+
182
+ With ``shared_pool=True`` the factory borrows the `channel.ChannelPool` of the default component —
183
+ an `AsyncChannelPoolProvider` there, or anything providing ``ChannelPool`` — instead of building
184
+ one, and owns nothing but its health checker: closing the container still stops the checker,
185
+ while the pool is closed by whoever provided it, once, after every factory that borrowed it.
186
+ ``shutdown_grace`` then has nothing to apply to; the pool provider carries its own. A container
187
+ with such a factory and no ``ChannelPool`` in the default component is refused when it is built.
119
188
  """
120
189
 
121
190
  scope = Scope.APP
@@ -125,13 +194,16 @@ class AsyncGrpcClientProvider(Provider):
125
194
  *,
126
195
  shutdown_grace: float | None = 5.0,
127
196
  ready_timeout: float | None = 10.0,
197
+ shared_pool: bool = False,
128
198
  component: str | None = None,
129
199
  ) -> None:
130
200
  super().__init__(component=component)
131
201
  self._shutdown_grace = shutdown_grace
132
202
  self._ready_timeout = ready_timeout
203
+ # One key, two sources, chosen here: a Dishka dependency cannot be optional, so a factory that
204
+ # asked for the pool would refuse every container without one.
205
+ self.provide(self.shared_factory if shared_pool else self.factory)
133
206
 
134
- @provide
135
207
  async def factory(
136
208
  self,
137
209
  settings: GrpcClientSettingsProtocol,
@@ -146,6 +218,21 @@ class AsyncGrpcClientProvider(Provider):
146
218
  ) as factory:
147
219
  yield factory
148
220
 
221
+ async def shared_factory(
222
+ self,
223
+ settings: GrpcClientSettingsProtocol,
224
+ metrics: GrpcClientMetricsProtocol | None,
225
+ pool: Annotated[ChannelPool, FromComponent(DEFAULT_COMPONENT)],
226
+ ) -> AsyncIterator[GrpcClientFactory]:
227
+ """Build the factory on the default component's pool, which it borrows and never closes."""
228
+ async with GrpcClientFactory(
229
+ settings=settings,
230
+ pool=pool,
231
+ ready_timeout=self._ready_timeout,
232
+ metrics=metrics,
233
+ ) as factory:
234
+ yield factory
235
+
149
236
 
150
237
  def grpc_client_providers(
151
238
  settings: GrpcClientSettingsProtocol,
@@ -154,6 +241,7 @@ def grpc_client_providers(
154
241
  metrics_prefix: str | None = None,
155
242
  shutdown_grace: float | None = 5.0,
156
243
  ready_timeout: float | None = 10.0,
244
+ shared_pool: bool = False,
157
245
  ) -> tuple[Provider, ...]:
158
246
  """Return the three providers of one upstream, for one-line registration.
159
247
 
@@ -162,8 +250,11 @@ def grpc_client_providers(
162
250
  component: The Dishka component to register the providers in — one per upstream when a
163
251
  container serves several, the default component otherwise.
164
252
  metrics_prefix: Prefix for the Prometheus metric names.
165
- shutdown_grace: Seconds in-flight RPCs get to finish when the container closes.
253
+ shutdown_grace: Seconds in-flight RPCs get to finish when the container closes; the pool
254
+ provider's own grace applies instead with ``shared_pool``.
166
255
  ready_timeout: Seconds the factory waits for the first health check pass when resolved.
256
+ shared_pool: Borrow the ``ChannelPool`` of the default component — an `AsyncChannelPoolProvider`
257
+ registered there — instead of building one for this upstream.
167
258
 
168
259
  Returns:
169
260
  The providers, to unpack into ``make_async_container``.
@@ -171,11 +262,14 @@ def grpc_client_providers(
171
262
  return (
172
263
  GrpcClientSettingsProvider(settings, component=component),
173
264
  PrometheusGrpcClientMetricsProvider(prefix=metrics_prefix, component=component),
174
- AsyncGrpcClientProvider(shutdown_grace=shutdown_grace, ready_timeout=ready_timeout, component=component),
265
+ AsyncGrpcClientProvider(
266
+ shutdown_grace=shutdown_grace, ready_timeout=ready_timeout, shared_pool=shared_pool, component=component
267
+ ),
175
268
  )
176
269
 
177
270
 
178
271
  __all__ = [
272
+ "AsyncChannelPoolProvider",
179
273
  "AsyncGrpcClientProvider",
180
274
  "GrpcClientSettingsProvider",
181
275
  "PrometheusGrpcClientMetricsProvider",
@@ -1 +0,0 @@
1
- __version__ = "0.3.0" # x-release-please-version
File without changes