grpc-client-kit 0.1.2__tar.gz → 0.2.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.2.0}/CHANGELOG.md +7 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/PKG-INFO +13 -3
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/README.md +9 -2
- grpc_client_kit-0.2.0/grpc_client_kit/__version__.py +1 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/factory.py +27 -6
- grpc_client_kit-0.2.0/grpc_client_kit/settings.py +418 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/pyproject.toml +5 -1
- grpc_client_kit-0.1.2/grpc_client_kit/__version__.py +0 -1
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/.gitignore +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/LICENSE +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/__init__.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/balancers.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/channel.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/client.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/config.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/deadline.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/errors.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/health.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/__init__.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/base.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/circuit_breaker.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/client_logging.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/context.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/deadline.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/metrics.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/outlier.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/retry.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/timeout.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/tracing.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/wait_for_ready.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/protocols.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/py.typed +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/utils.py +0 -0
- {grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/validation.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [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)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* settings models for the environment, mirroring the dataclasses ([edffb5f](https://github.com/bedrock-python/grpc-client-kit/commit/edffb5fdcf934cecd5788a99e37064133383d9cd))
|
|
9
|
+
|
|
3
10
|
## [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
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: grpc-client-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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/
|
|
@@ -226,6 +226,7 @@ Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'all'
|
|
|
226
226
|
Requires-Dist: grpcio-health-checking<2.0.0,>=1.78.0; extra == 'all'
|
|
227
227
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'all'
|
|
228
228
|
Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'all'
|
|
229
|
+
Requires-Dist: pydantic<3.0,>=2.0.0; extra == 'all'
|
|
229
230
|
Provides-Extra: deadline
|
|
230
231
|
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'deadline'
|
|
231
232
|
Provides-Extra: health
|
|
@@ -235,6 +236,8 @@ Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'metrics'
|
|
|
235
236
|
Provides-Extra: observability
|
|
236
237
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'observability'
|
|
237
238
|
Requires-Dist: prometheus-client<1.0.0,>=0.20.0; extra == 'observability'
|
|
239
|
+
Provides-Extra: settings
|
|
240
|
+
Requires-Dist: pydantic<3.0,>=2.0.0; extra == 'settings'
|
|
238
241
|
Provides-Extra: tracing
|
|
239
242
|
Requires-Dist: opentelemetry-api<2.0.0,>=1.30.0; extra == 'tracing'
|
|
240
243
|
Description-Content-Type: text/markdown
|
|
@@ -307,6 +310,7 @@ pip install "grpc-client-kit[health]" # + grpc.health.v1 monitoring
|
|
|
307
310
|
pip install "grpc-client-kit[metrics,tracing]" # + Prometheus + OpenTelemetry
|
|
308
311
|
pip install "grpc-client-kit[observability]" # + metrics and tracing together
|
|
309
312
|
pip install "grpc-client-kit[deadline]" # + request budget propagation
|
|
313
|
+
pip install "grpc-client-kit[settings]" # + pydantic settings models
|
|
310
314
|
pip install "grpc-client-kit[all]" # everything
|
|
311
315
|
```
|
|
312
316
|
|
|
@@ -362,7 +366,11 @@ Any object carrying the fields of `GrpcClientSettingsProtocol` works —
|
|
|
362
366
|
a pydantic model, a dataclass, a plain class. Everything optional
|
|
363
367
|
(`credentials`, `options`, `compression`, `metrics_registry`,
|
|
364
368
|
`sensitive_headers`) is read through `getattr`, so a minimal settings object
|
|
365
|
-
stays valid.
|
|
369
|
+
stays valid. Or take the shipped one: `grpc_client_kit.settings.BaseGrpcClientSettings`
|
|
370
|
+
(the `[settings]` extra) is that shape written down once — plain pydantic
|
|
371
|
+
models with the kit's own defaults and bounds, one per upstream, nested under
|
|
372
|
+
your service's `BaseSettings` for loading from the environment, and
|
|
373
|
+
`to_config()` on every section for a hand-built chain. See the
|
|
366
374
|
[configuration guide](https://bedrock-python.github.io/grpc-client-kit/guide/configuration/)
|
|
367
375
|
for the full field list.
|
|
368
376
|
|
|
@@ -511,6 +519,7 @@ names the extra to install.
|
|
|
511
519
|
| `channel.ChannelPool` | Channels pooled by full identity, idle eviction, health flags | core |
|
|
512
520
|
| `client.GrpcClient` | Stub factory: target selection + channel acquisition | core |
|
|
513
521
|
| `factory.GrpcClientFactory` | Settings → pool, balancer, health checker, per-target chains | core |
|
|
522
|
+
| `settings.BaseGrpcClientSettings` | The settings shape as pydantic models mirroring the dataclasses, `to_config()` on each | `settings` |
|
|
514
523
|
| `balancers.*` | Round-robin / random / weighted selection with health filtering | core |
|
|
515
524
|
| `interceptors.*` | Context, logging, timeout, wait-for-ready, retry, circuit breaker + chain builder | core |
|
|
516
525
|
| `deadline` + `interceptors.deadline` | The request budget in a contextvar, and the layer that trims every call to it | `deadline` |
|
|
@@ -530,8 +539,9 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
|
|
|
530
539
|
| `tracing` | `opentelemetry-api` | `AsyncClientTracingInterceptor` (a pass-through without it) |
|
|
531
540
|
| `metrics` | `prometheus-client` | the default metrics backend; a custom registry needs no extra |
|
|
532
541
|
| `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
|
|
542
|
+
| `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
|
|
533
543
|
| `observability` | `metrics` + `tracing` | both of the above |
|
|
534
|
-
| `all` | `deadline` + `health` + `metrics` + `tracing` | everything |
|
|
544
|
+
| `all` | `deadline` + `health` + `metrics` + `settings` + `tracing` | everything |
|
|
535
545
|
|
|
536
546
|
`deadline` is deliberately not part of `observability`: propagating a budget is
|
|
537
547
|
resilience, not telemetry, and an observability extra should not pull in a
|
|
@@ -66,6 +66,7 @@ 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
|
|
69
70
|
pip install "grpc-client-kit[all]" # everything
|
|
70
71
|
```
|
|
71
72
|
|
|
@@ -121,7 +122,11 @@ Any object carrying the fields of `GrpcClientSettingsProtocol` works —
|
|
|
121
122
|
a pydantic model, a dataclass, a plain class. Everything optional
|
|
122
123
|
(`credentials`, `options`, `compression`, `metrics_registry`,
|
|
123
124
|
`sensitive_headers`) is read through `getattr`, so a minimal settings object
|
|
124
|
-
stays valid.
|
|
125
|
+
stays valid. Or take the shipped one: `grpc_client_kit.settings.BaseGrpcClientSettings`
|
|
126
|
+
(the `[settings]` extra) is that shape written down once — plain pydantic
|
|
127
|
+
models with the kit's own defaults and bounds, one per upstream, nested under
|
|
128
|
+
your service's `BaseSettings` for loading from the environment, and
|
|
129
|
+
`to_config()` on every section for a hand-built chain. See the
|
|
125
130
|
[configuration guide](https://bedrock-python.github.io/grpc-client-kit/guide/configuration/)
|
|
126
131
|
for the full field list.
|
|
127
132
|
|
|
@@ -270,6 +275,7 @@ names the extra to install.
|
|
|
270
275
|
| `channel.ChannelPool` | Channels pooled by full identity, idle eviction, health flags | core |
|
|
271
276
|
| `client.GrpcClient` | Stub factory: target selection + channel acquisition | core |
|
|
272
277
|
| `factory.GrpcClientFactory` | Settings → pool, balancer, health checker, per-target chains | core |
|
|
278
|
+
| `settings.BaseGrpcClientSettings` | The settings shape as pydantic models mirroring the dataclasses, `to_config()` on each | `settings` |
|
|
273
279
|
| `balancers.*` | Round-robin / random / weighted selection with health filtering | core |
|
|
274
280
|
| `interceptors.*` | Context, logging, timeout, wait-for-ready, retry, circuit breaker + chain builder | core |
|
|
275
281
|
| `deadline` + `interceptors.deadline` | The request budget in a contextvar, and the layer that trims every call to it | `deadline` |
|
|
@@ -289,8 +295,9 @@ with any backend you pass. The `[metrics]` extra only supplies the usual one.
|
|
|
289
295
|
| `tracing` | `opentelemetry-api` | `AsyncClientTracingInterceptor` (a pass-through without it) |
|
|
290
296
|
| `metrics` | `prometheus-client` | the default metrics backend; a custom registry needs no extra |
|
|
291
297
|
| `deadline` | `deadline-budget` | `AsyncDeadlineBudgetInterceptor` (the layer is skipped without it) |
|
|
298
|
+
| `settings` | `pydantic` | `BaseGrpcClientSettings` and the section models |
|
|
292
299
|
| `observability` | `metrics` + `tracing` | both of the above |
|
|
293
|
-
| `all` | `deadline` + `health` + `metrics` + `tracing` | everything |
|
|
300
|
+
| `all` | `deadline` + `health` + `metrics` + `settings` + `tracing` | everything |
|
|
294
301
|
|
|
295
302
|
`deadline` is deliberately not part of `observability`: propagating a budget is
|
|
296
303
|
resilience, not telemetry, and an observability extra should not pull in a
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0" # x-release-please-version
|
|
@@ -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.
|
|
@@ -306,6 +311,10 @@ class GrpcClientFactory:
|
|
|
306
311
|
metrics_registry=metrics_registry,
|
|
307
312
|
service_name=actual_service_name,
|
|
308
313
|
sensitive_headers=getattr(self._settings, "sensitive_headers", None),
|
|
314
|
+
sensitive_methods=getattr(self._settings, "sensitive_methods", None),
|
|
315
|
+
sensitive_patterns=getattr(self._settings, "sensitive_patterns", None),
|
|
316
|
+
log_request_payload=getattr(self._settings, "log_request_payload", False),
|
|
317
|
+
log_response_payload=getattr(self._settings, "log_response_payload", False),
|
|
309
318
|
enable_method_label=getattr(self._settings, "enable_method_label", True),
|
|
310
319
|
success_log_level=getattr(self._settings, "success_log_level", logging.INFO),
|
|
311
320
|
)
|
|
@@ -371,8 +380,8 @@ class GrpcClientFactory:
|
|
|
371
380
|
Channel construction settings are optional blocks (see `protocols.GrpcChannelExtrasProtocol`)
|
|
372
381
|
and are read with `getattr`, so a settings model that declares none of them still satisfies
|
|
373
382
|
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.
|
|
383
|
+
`config.ConnectivityConfig` — or a block with its fields — under that name has its channels
|
|
384
|
+
tuned by it, and one that does not is left with gRPC's defaults.
|
|
376
385
|
"""
|
|
377
386
|
if self._settings:
|
|
378
387
|
actual_target = target or self._settings.target
|
|
@@ -380,7 +389,7 @@ class GrpcClientFactory:
|
|
|
380
389
|
credentials = getattr(self._settings, "credentials", None)
|
|
381
390
|
options = getattr(self._settings, "options", None)
|
|
382
391
|
compression = getattr(self._settings, "compression", None)
|
|
383
|
-
connectivity = getattr(self._settings, "connectivity", None)
|
|
392
|
+
connectivity = self._build_connectivity_config(getattr(self._settings, "connectivity", None))
|
|
384
393
|
else:
|
|
385
394
|
actual_target = target
|
|
386
395
|
insecure = False
|
|
@@ -422,11 +431,23 @@ class GrpcClientFactory:
|
|
|
422
431
|
health_checker=self._health_checker,
|
|
423
432
|
)
|
|
424
433
|
|
|
434
|
+
def _build_connectivity_config(self, s: Any | None) -> ConnectivityConfig | None:
|
|
435
|
+
"""Build ConnectivityConfig from an optional settings block (duck-typed).
|
|
436
|
+
|
|
437
|
+
A ready `config.ConnectivityConfig` is taken as it is. Any other block is read field by
|
|
438
|
+
field, and a field it does not carry keeps the dataclass's own default.
|
|
439
|
+
"""
|
|
440
|
+
if not s:
|
|
441
|
+
return None
|
|
442
|
+
if isinstance(s, ConnectivityConfig):
|
|
443
|
+
return s
|
|
444
|
+
return ConnectivityConfig(**{name: getattr(s, name) for name in _CONNECTIVITY_FIELDS if hasattr(s, name)})
|
|
445
|
+
|
|
425
446
|
def _build_timeout_config(self, s: TimeoutSettingsProtocol | None) -> TimeoutConfig | None:
|
|
426
|
-
"""Build TimeoutConfig from settings protocol."""
|
|
447
|
+
"""Build TimeoutConfig from settings protocol; per-method budgets are read when the block has them."""
|
|
427
448
|
if not s:
|
|
428
449
|
return None
|
|
429
|
-
return TimeoutConfig(default=s.default)
|
|
450
|
+
return TimeoutConfig(default=s.default, per_method=dict(getattr(s, "per_method", None) or {}))
|
|
430
451
|
|
|
431
452
|
def _build_retry_config(
|
|
432
453
|
self, s: RetrySettingsProtocol | None, metrics: GrpcClientMetricsProtocol | None = None
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""The settings shape the factory reads, as pydantic models, written down once.
|
|
2
|
+
|
|
3
|
+
`factory.GrpcClientFactory` reads a settings object structurally — `protocols.GrpcClientSettingsProtocol`
|
|
4
|
+
and the optional blocks around it — so every service used to write its own models for that shape and
|
|
5
|
+
its own mapping onto the kit's dataclasses, and every copy drifted in its own way. These models are
|
|
6
|
+
that shape with the kit's own defaults and bounds; each one that mirrors a dataclass knows how to
|
|
7
|
+
become it (``to_config()``), and the kit's own tests pin the two field sets to each other.
|
|
8
|
+
|
|
9
|
+
Every class here is a plain :class:`pydantic.BaseModel`, deliberately. A section that were a
|
|
10
|
+
``BaseSettings`` would read the environment on its own, with no prefix, and a bare ``TARGET`` or
|
|
11
|
+
``TIMEOUT`` in a pod would land in it. Nest :class:`BaseGrpcClientSettings` under the service's own
|
|
12
|
+
settings instead, once per upstream, and let that class own the environment::
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
16
|
+
|
|
17
|
+
from grpc_client_kit import GrpcClientFactory
|
|
18
|
+
from grpc_client_kit.settings import BaseGrpcClientSettings
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Settings(BaseSettings):
|
|
22
|
+
model_config = SettingsConfigDict(env_nested_delimiter="__")
|
|
23
|
+
|
|
24
|
+
users_grpc: BaseGrpcClientSettings = Field(default_factory=BaseGrpcClientSettings)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# USERS_GRPC__TARGET=users.internal:50051 USERS_GRPC__RETRY__MAX_ATTEMPTS=5 python main.py
|
|
28
|
+
settings = Settings()
|
|
29
|
+
factory = GrpcClientFactory(settings=settings.users_grpc)
|
|
30
|
+
|
|
31
|
+
Runtime objects are not settings and have no field here: channel credentials are handed to
|
|
32
|
+
:meth:`BaseGrpcClientSettings.to_config`, metrics registries and retry callbacks to the factory or to
|
|
33
|
+
the chain built from the sections. Unknown fields are refused, so a misspelled variable fails at load
|
|
34
|
+
instead of quietly yielding a default.
|
|
35
|
+
|
|
36
|
+
This module needs the ``settings`` extra (``grpc-client-kit[settings]``), which pulls in pydantic;
|
|
37
|
+
importing it without raises an ``ImportError`` naming the extra.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import logging
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
import grpc
|
|
46
|
+
|
|
47
|
+
from .balancers import LoadBalancerConfig, LoadBalancingStrategy
|
|
48
|
+
from .config import ConnectivityConfig, GrpcClientConfig
|
|
49
|
+
from .interceptors import (
|
|
50
|
+
CircuitBreakerConfig,
|
|
51
|
+
DeadlineBudgetConfig,
|
|
52
|
+
RetryConfig,
|
|
53
|
+
TimeoutConfig,
|
|
54
|
+
WaitForReadyConfig,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
59
|
+
except ImportError as exc:
|
|
60
|
+
raise ImportError("Install grpc-client-kit[settings] (pydantic) to use the settings models") from exc
|
|
61
|
+
|
|
62
|
+
# The names an operator writes for `grpc.Compression`. "none" is gRPC's explicit no-compression
|
|
63
|
+
# setting, not the absence of one — that is ``None``, which leaves the gRPC default alone.
|
|
64
|
+
_COMPRESSION_BY_NAME: dict[str, grpc.Compression] = {
|
|
65
|
+
"none": grpc.Compression.NoCompression,
|
|
66
|
+
"deflate": grpc.Compression.Deflate,
|
|
67
|
+
"gzip": grpc.Compression.Gzip,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _status_code_by_name(value: object) -> object:
|
|
72
|
+
"""Resolve a status code written by name (``UNAVAILABLE``); anything else is left to pydantic."""
|
|
73
|
+
if not isinstance(value, str):
|
|
74
|
+
return value
|
|
75
|
+
try:
|
|
76
|
+
return grpc.StatusCode[value.upper()]
|
|
77
|
+
except KeyError:
|
|
78
|
+
names = ", ".join(code.name for code in grpc.StatusCode)
|
|
79
|
+
raise ValueError(f"unknown gRPC status code {value!r}; expected one of {names}") from None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BaseConnectivitySettings(BaseModel):
|
|
83
|
+
"""``connectivity``: keepalive and reconnect backoff in seconds, `config.ConnectivityConfig` by environment.
|
|
84
|
+
|
|
85
|
+
Opt-in like the dataclass: an absent block leaves every channel argument at gRPC's own default,
|
|
86
|
+
and ``None`` in a field does the same for that argument alone.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
model_config = ConfigDict(extra="forbid")
|
|
90
|
+
|
|
91
|
+
keepalive_time: float | None = Field(
|
|
92
|
+
default=30.0, gt=0, description="Seconds of inactivity before a keepalive ping (None: never ping)"
|
|
93
|
+
)
|
|
94
|
+
keepalive_timeout: float | None = Field(default=10.0, gt=0, description="Seconds to wait for the ping's answer")
|
|
95
|
+
permit_without_calls: bool = Field(default=False, description="Keep pinging while no call is in flight")
|
|
96
|
+
max_pings_without_data: int | None = Field(
|
|
97
|
+
default=2, ge=0, description="Pings allowed on a connection carrying no data (0: no limit)"
|
|
98
|
+
)
|
|
99
|
+
initial_reconnect_backoff: float | None = Field(
|
|
100
|
+
default=1.0, gt=0, description="Seconds before the first reconnect attempt"
|
|
101
|
+
)
|
|
102
|
+
min_reconnect_backoff: float | None = Field(
|
|
103
|
+
default=None, gt=0, description="Lower bound between reconnect attempts (None: gRPC's default)"
|
|
104
|
+
)
|
|
105
|
+
max_reconnect_backoff: float | None = Field(
|
|
106
|
+
default=30.0, gt=0, description="Upper bound between reconnect attempts"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def to_config(self) -> ConnectivityConfig:
|
|
110
|
+
"""Build the `config.ConnectivityConfig` these fields describe.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If the reconnect bounds contradict each other; the dataclass keeps that check.
|
|
114
|
+
"""
|
|
115
|
+
return ConnectivityConfig(**self.model_dump())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BaseChannelPoolSettings(BaseModel):
|
|
119
|
+
"""``pool``: what `channel.ChannelPool` is sized with, read by the factory."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="forbid")
|
|
122
|
+
|
|
123
|
+
max_channels_per_target: int = Field(default=1, ge=1, description="Channels per channel identity")
|
|
124
|
+
idle_timeout: float = Field(
|
|
125
|
+
default=300.0, description="Seconds without active RPCs before gRPC parks a connection (0: never)"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BaseTimeoutSettings(BaseModel):
|
|
130
|
+
"""``timeout``: the budget of a whole call, retries included; `interceptors.TimeoutConfig` by environment."""
|
|
131
|
+
|
|
132
|
+
model_config = ConfigDict(extra="forbid")
|
|
133
|
+
|
|
134
|
+
default: float = Field(
|
|
135
|
+
default=10.0, ge=0, description="Budget in seconds for methods not listed in per_method (0: no deadline)"
|
|
136
|
+
)
|
|
137
|
+
per_method: dict[str, float | None] = Field(
|
|
138
|
+
default_factory=dict,
|
|
139
|
+
description='Budgets by full method name, as JSON: {"/pkg.Service/Method": 60} (null: no deadline)',
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def to_config(self) -> TimeoutConfig:
|
|
143
|
+
"""Build the `interceptors.TimeoutConfig` these fields describe."""
|
|
144
|
+
return TimeoutConfig(**self.model_dump())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BaseRetrySettings(BaseModel):
|
|
148
|
+
"""``retry``: the retry policy, `interceptors.RetryConfig` by environment.
|
|
149
|
+
|
|
150
|
+
Status codes are written by name (``UNAVAILABLE``). ``on_retry`` and ``metrics`` are runtime
|
|
151
|
+
objects and have no field here; the factory injects the registry, a hand-built chain sets both on
|
|
152
|
+
the config `to_config` returns.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
model_config = ConfigDict(extra="forbid")
|
|
156
|
+
|
|
157
|
+
max_attempts: int = Field(default=3, ge=1, description="Total attempts, the first one included")
|
|
158
|
+
initial_backoff: float = Field(default=0.1, ge=0, description="Seconds before the first retry")
|
|
159
|
+
max_backoff: float = Field(default=10.0, ge=0, description="Upper bound for the wait between retries")
|
|
160
|
+
backoff_multiplier: float = Field(default=2.0, ge=1, description="Growth factor of the backoff per attempt")
|
|
161
|
+
jitter: float = Field(default=0.1, ge=0, le=1, description="Random variation applied to the backoff")
|
|
162
|
+
retryable_codes: set[grpc.StatusCode] | None = Field(
|
|
163
|
+
default=None, description="Status codes that trigger a retry, by name (None: the kit's default set)"
|
|
164
|
+
)
|
|
165
|
+
retry_streaming: bool = Field(default=False, description="Allow retrying unary-stream calls at all")
|
|
166
|
+
idempotent_methods: set[str] | None = Field(
|
|
167
|
+
default=None, description="Full method names that may be retried; a whitelist once given"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@field_validator("retryable_codes", mode="before")
|
|
171
|
+
@classmethod
|
|
172
|
+
def _codes_by_name(cls, value: object) -> object:
|
|
173
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
174
|
+
return {_status_code_by_name(item) for item in value}
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
def to_config(self) -> RetryConfig:
|
|
178
|
+
"""Build the `interceptors.RetryConfig` these fields describe, with no callback and no registry."""
|
|
179
|
+
return RetryConfig(**self.model_dump())
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class BaseCircuitBreakerSettings(BaseModel):
|
|
183
|
+
"""``circuit_breaker``: per-method, per-target breaking; `interceptors.CircuitBreakerConfig` by environment.
|
|
184
|
+
|
|
185
|
+
``metrics`` is a runtime object and has no field here; the factory injects the registry.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
model_config = ConfigDict(extra="forbid")
|
|
189
|
+
|
|
190
|
+
fail_threshold: int = Field(default=5, ge=1, description="Consecutive failed attempts that open a circuit")
|
|
191
|
+
recovery_timeout: float = Field(default=60.0, ge=0, description="Seconds an open circuit waits before a trial")
|
|
192
|
+
half_open_max_calls: int = Field(default=1, ge=1, description="Trial calls admitted while half-open")
|
|
193
|
+
max_methods: int = Field(default=1000, ge=1, description="Methods tracked per breaker before the LRU evicts")
|
|
194
|
+
|
|
195
|
+
def to_config(self) -> CircuitBreakerConfig:
|
|
196
|
+
"""Build the `interceptors.CircuitBreakerConfig` these fields describe, with no registry."""
|
|
197
|
+
return CircuitBreakerConfig(**self.model_dump())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class BaseWaitForReadySettings(BaseModel):
|
|
201
|
+
"""``wait_for_ready``: wait for a connection instead of failing fast; `interceptors.WaitForReadyConfig`."""
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(extra="forbid")
|
|
204
|
+
|
|
205
|
+
default: bool | None = Field(
|
|
206
|
+
default=True, description="Value for methods not listed in per_method (None: untouched)"
|
|
207
|
+
)
|
|
208
|
+
per_method: dict[str, bool | None] = Field(
|
|
209
|
+
default_factory=dict, description='Values by full method name, as JSON: {"/pkg.Service/Method": false}'
|
|
210
|
+
)
|
|
211
|
+
require_deadline: bool = Field(default=True, description="Wait only on calls that carry a deadline")
|
|
212
|
+
|
|
213
|
+
def to_config(self) -> WaitForReadyConfig:
|
|
214
|
+
"""Build the `interceptors.WaitForReadyConfig` these fields describe."""
|
|
215
|
+
return WaitForReadyConfig(**self.model_dump())
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class BaseDeadlineBudgetSettings(BaseModel):
|
|
219
|
+
"""``deadline_budget``: request budget propagation; `interceptors.DeadlineBudgetConfig` by environment.
|
|
220
|
+
|
|
221
|
+
Needs the ``deadline`` extra to take effect; without it the layer is skipped with a warning.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
model_config = ConfigDict(extra="forbid")
|
|
225
|
+
|
|
226
|
+
reserve_for_next: float = Field(
|
|
227
|
+
default=0.0, ge=0, description="Seconds every call keeps back for the work that follows it"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def to_config(self) -> DeadlineBudgetConfig:
|
|
231
|
+
"""Build the `interceptors.DeadlineBudgetConfig` these fields describe."""
|
|
232
|
+
return DeadlineBudgetConfig(**self.model_dump())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class BaseLoadBalancerSettings(BaseModel):
|
|
236
|
+
"""``balancer``: how a target is picked from ``targets``; `balancers.LoadBalancerConfig` by environment."""
|
|
237
|
+
|
|
238
|
+
model_config = ConfigDict(extra="forbid")
|
|
239
|
+
|
|
240
|
+
strategy: LoadBalancingStrategy = Field(
|
|
241
|
+
default=LoadBalancingStrategy.ROUND_ROBIN, description="round_robin, random or weighted"
|
|
242
|
+
)
|
|
243
|
+
weights: dict[str, float] | None = Field(
|
|
244
|
+
default=None, description='Weights by target for the weighted strategy, as JSON: {"host:50051": 2.0}'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def to_config(self) -> LoadBalancerConfig:
|
|
248
|
+
"""Build the `balancers.LoadBalancerConfig` these fields describe."""
|
|
249
|
+
return LoadBalancerConfig(**self.model_dump())
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class BaseHealthCheckerSettings(BaseModel):
|
|
253
|
+
"""``health_checker``: active ``grpc.health.v1`` probing of ``targets``, read by the factory.
|
|
254
|
+
|
|
255
|
+
Needs the ``health`` extra: the factory raises ``ImportError`` naming it when this block is set.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
model_config = ConfigDict(extra="forbid")
|
|
259
|
+
|
|
260
|
+
check_interval: float = Field(default=30.0, gt=0, description="Seconds between probes of a healthy target")
|
|
261
|
+
timeout: float = Field(default=5.0, gt=0, description="Seconds one probe may take")
|
|
262
|
+
service: str = Field(default="", description="Service name probed; empty asks about the server as a whole")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class BaseGrpcClientSettings(BaseModel):
|
|
266
|
+
"""One upstream's client settings: what the factory reads, and `config.GrpcClientConfig` by `to_config`.
|
|
267
|
+
|
|
268
|
+
Satisfies `protocols.GrpcClientSettingsProtocol` and carries every optional block the factory
|
|
269
|
+
reads, so an instance is handed to `factory.GrpcClientFactory` as it is. The observability flags
|
|
270
|
+
are flat because that is how the protocol spells them; the rest of
|
|
271
|
+
`interceptors.ObservabilityConfig` sits next to them under the dataclass's own names, minus
|
|
272
|
+
``service_name`` (the factory names each client after its stub) and the registry.
|
|
273
|
+
|
|
274
|
+
Two blocks are present by default: ``pool``, at the pool's own defaults, and ``timeout``, because
|
|
275
|
+
a client without a deadline is the one default this kit will not ship — ``TIMEOUT__DEFAULT=0``
|
|
276
|
+
switches it off. Everything that adds a layer or needs an extra — retries, the breaker,
|
|
277
|
+
wait-for-ready, budget propagation, balancing, health checks, connectivity tuning — is ``None``
|
|
278
|
+
until configured; with pydantic-settings a nested block is created the moment one of its
|
|
279
|
+
variables is set. Subclass it to change a default or add fields of your own::
|
|
280
|
+
|
|
281
|
+
class UsersGrpcSettings(BaseGrpcClientSettings):
|
|
282
|
+
retry: BaseRetrySettings | None = Field(default_factory=BaseRetrySettings)
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
model_config = ConfigDict(extra="forbid")
|
|
286
|
+
|
|
287
|
+
# Channel
|
|
288
|
+
target: str | None = Field(
|
|
289
|
+
default=None, description="Single target, host:port or a gRPC resolver URI; exclusive with targets"
|
|
290
|
+
)
|
|
291
|
+
targets: list[str] | None = Field(default=None, description="Targets to balance across; exclusive with target")
|
|
292
|
+
insecure: bool = Field(
|
|
293
|
+
default=False,
|
|
294
|
+
description="Plaintext channel (False: TLS, from the system trust store or to_config's credentials)",
|
|
295
|
+
)
|
|
296
|
+
options: list[tuple[str, Any]] | None = Field(
|
|
297
|
+
default=None, description='Raw channel arguments, as JSON pairs: [["grpc.max_receive_message_length", 8388608]]'
|
|
298
|
+
)
|
|
299
|
+
compression: grpc.Compression | None = Field(
|
|
300
|
+
default=None, description="Channel compression by name: none, deflate or gzip (None: the gRPC default)"
|
|
301
|
+
)
|
|
302
|
+
connectivity: BaseConnectivitySettings | None = Field(
|
|
303
|
+
default=None, description="Keepalive and reconnect tuning (None: gRPC's defaults)"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Observability, flat: this is how GrpcClientSettingsProtocol spells the flags
|
|
307
|
+
tracing_enabled: bool = Field(default=False, description="Add the tracing layer (needs the tracing extra)")
|
|
308
|
+
metrics_enabled: bool = Field(default=False, description="Add the metrics layer (needs a registry)")
|
|
309
|
+
logging_enabled: bool = Field(default=True, description="Add the structured logging layer")
|
|
310
|
+
sensitive_headers: set[str] | None = Field(
|
|
311
|
+
default=None, description="Header names redacted in logged metadata (None: the kit's default set)"
|
|
312
|
+
)
|
|
313
|
+
sensitive_methods: set[str] | None = Field(
|
|
314
|
+
default=None, description="Full method names whose payloads and errors are kept out of the logs"
|
|
315
|
+
)
|
|
316
|
+
sensitive_patterns: list[str] | None = Field(
|
|
317
|
+
default=None, description="Regex patterns marking methods as sensitive"
|
|
318
|
+
)
|
|
319
|
+
log_request_payload: bool = Field(default=False, description="Log request payloads, truncated")
|
|
320
|
+
log_response_payload: bool = Field(default=False, description="Log response payloads, truncated")
|
|
321
|
+
enable_method_label: bool = Field(
|
|
322
|
+
default=True, description="Label metrics by method (off for services with thousands of methods)"
|
|
323
|
+
)
|
|
324
|
+
success_log_level: int = Field(
|
|
325
|
+
default=logging.INFO, description="Level of the record a successful call emits, by name or number"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# The blocks the factory maps onto the pool, the chain, the balancer and the health checker
|
|
329
|
+
pool: BaseChannelPoolSettings | None = Field(
|
|
330
|
+
default_factory=BaseChannelPoolSettings, description="Channel pool sizing"
|
|
331
|
+
)
|
|
332
|
+
timeout: BaseTimeoutSettings | None = Field(
|
|
333
|
+
default_factory=BaseTimeoutSettings, description="Call budgets (None: no timeout layer, so no deadline at all)"
|
|
334
|
+
)
|
|
335
|
+
retry: BaseRetrySettings | None = Field(default=None, description="Retry policy (None: no retry layer)")
|
|
336
|
+
circuit_breaker: BaseCircuitBreakerSettings | None = Field(
|
|
337
|
+
default=None, description="Circuit breaker (None: no breaker layer)"
|
|
338
|
+
)
|
|
339
|
+
wait_for_ready: BaseWaitForReadySettings | None = Field(
|
|
340
|
+
default=None, description="Wait for a connection instead of failing fast (None: gRPC's fail-fast)"
|
|
341
|
+
)
|
|
342
|
+
deadline_budget: BaseDeadlineBudgetSettings | None = Field(
|
|
343
|
+
default=None, description="Request budget propagation (None: none; needs the deadline extra)"
|
|
344
|
+
)
|
|
345
|
+
balancer: BaseLoadBalancerSettings | None = Field(
|
|
346
|
+
default=None, description="Strategy over targets (None: round-robin)"
|
|
347
|
+
)
|
|
348
|
+
health_checker: BaseHealthCheckerSettings | None = Field(
|
|
349
|
+
default=None, description="Active probing of targets (None: none; needs the health extra)"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
@field_validator("compression", mode="before")
|
|
353
|
+
@classmethod
|
|
354
|
+
def _compression_by_name(cls, value: object) -> object:
|
|
355
|
+
if not isinstance(value, str):
|
|
356
|
+
return value
|
|
357
|
+
try:
|
|
358
|
+
return _COMPRESSION_BY_NAME[value.lower()]
|
|
359
|
+
except KeyError:
|
|
360
|
+
names = ", ".join(_COMPRESSION_BY_NAME)
|
|
361
|
+
raise ValueError(f"unknown compression {value!r}; expected one of {names}") from None
|
|
362
|
+
|
|
363
|
+
@field_validator("success_log_level", mode="before")
|
|
364
|
+
@classmethod
|
|
365
|
+
def _level_by_name(cls, value: object) -> object:
|
|
366
|
+
# An environment hands over strings, so a numeric one is left to pydantic's int coercion.
|
|
367
|
+
if not isinstance(value, str) or value.isdecimal():
|
|
368
|
+
return value
|
|
369
|
+
try:
|
|
370
|
+
return logging.getLevelNamesMapping()[value.upper()]
|
|
371
|
+
except KeyError:
|
|
372
|
+
raise ValueError(f"unknown log level {value!r}") from None
|
|
373
|
+
|
|
374
|
+
@model_validator(mode="after")
|
|
375
|
+
def _one_way_to_name_the_backend(self) -> BaseGrpcClientSettings:
|
|
376
|
+
if self.target is not None and self.targets:
|
|
377
|
+
raise ValueError("target and targets are mutually exclusive: one address, or a list to balance across")
|
|
378
|
+
return self
|
|
379
|
+
|
|
380
|
+
def to_config(self, *, credentials: grpc.ChannelCredentials | None = None) -> GrpcClientConfig:
|
|
381
|
+
"""Build the `config.GrpcClientConfig` these fields describe.
|
|
382
|
+
|
|
383
|
+
The chain is built from the sections (``settings.retry.to_config()`` and so on, into
|
|
384
|
+
`interceptors.build_interceptors`); the factory does both from the same instance.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
credentials: TLS credentials for the channel. A credentials object is built in code, not
|
|
388
|
+
read from an environment, so it is handed in here — and ``insecure=True`` together with
|
|
389
|
+
credentials is refused by the dataclass, exactly as it always was.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
The channel configuration.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
ValueError: If the configuration contradicts itself; the dataclasses keep those checks.
|
|
396
|
+
"""
|
|
397
|
+
return GrpcClientConfig(
|
|
398
|
+
target=self.target,
|
|
399
|
+
insecure=self.insecure,
|
|
400
|
+
credentials=credentials,
|
|
401
|
+
options=self.options,
|
|
402
|
+
compression=self.compression,
|
|
403
|
+
connectivity=self.connectivity.to_config() if self.connectivity is not None else None,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
__all__ = [
|
|
408
|
+
"BaseChannelPoolSettings",
|
|
409
|
+
"BaseCircuitBreakerSettings",
|
|
410
|
+
"BaseConnectivitySettings",
|
|
411
|
+
"BaseDeadlineBudgetSettings",
|
|
412
|
+
"BaseGrpcClientSettings",
|
|
413
|
+
"BaseHealthCheckerSettings",
|
|
414
|
+
"BaseLoadBalancerSettings",
|
|
415
|
+
"BaseRetrySettings",
|
|
416
|
+
"BaseTimeoutSettings",
|
|
417
|
+
"BaseWaitForReadySettings",
|
|
418
|
+
]
|
|
@@ -41,11 +41,14 @@ metrics = [
|
|
|
41
41
|
deadline = [
|
|
42
42
|
"deadline-budget>=0.1.0,<1.0.0",
|
|
43
43
|
]
|
|
44
|
+
settings = [
|
|
45
|
+
"pydantic>=2.0.0,<3.0",
|
|
46
|
+
]
|
|
44
47
|
observability = [
|
|
45
48
|
"grpc-client-kit[metrics,tracing]",
|
|
46
49
|
]
|
|
47
50
|
all = [
|
|
48
|
-
"grpc-client-kit[deadline,health,metrics,tracing]",
|
|
51
|
+
"grpc-client-kit[deadline,health,metrics,settings,tracing]",
|
|
49
52
|
]
|
|
50
53
|
|
|
51
54
|
[dependency-groups]
|
|
@@ -55,6 +58,7 @@ test = [
|
|
|
55
58
|
"pytest-cov>=7.0.0",
|
|
56
59
|
"pytest-lazy-fixtures>=1.1.1",
|
|
57
60
|
"freezegun>=1.5.5",
|
|
61
|
+
"pydantic-settings>=2.0.0,<3.0",
|
|
58
62
|
]
|
|
59
63
|
docs = [
|
|
60
64
|
"zensical>=0.0.37",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2" # x-release-please-version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/circuit_breaker.py
RENAMED
|
File without changes
|
{grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/client_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{grpc_client_kit-0.1.2 → grpc_client_kit-0.2.0}/grpc_client_kit/interceptors/wait_for_ready.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|