kurrentdbclient 1.3.3__tar.gz → 1.3.5__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 (63) hide show
  1. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/PKG-INFO +7 -6
  2. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/README.md +5 -5
  3. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/__init__.py +1 -1
  4. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/asyncio_client.py +18 -6
  5. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/client.py +183 -14
  6. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/common.py +134 -32
  7. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/connection.py +2 -2
  8. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/exceptions.py +8 -2
  9. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/spanners.py +8 -12
  10. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/persistent.py +29 -19
  11. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/streams.py +48 -24
  12. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/pyproject.toml +3 -2
  13. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/LICENSE +0 -0
  14. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/connection_spec.py +0 -0
  15. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/events.py +0 -0
  16. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/gossip.py +0 -0
  17. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/__init__.py +0 -0
  18. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/__init__.py +0 -0
  19. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/attributes.py +0 -0
  20. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/grpc.py +0 -0
  21. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/package.py +0 -0
  22. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/utils.py +0 -0
  23. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/version.py +0 -0
  24. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/projections.py +0 -0
  25. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2.py +0 -0
  26. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2.pyi +0 -0
  27. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2_grpc.py +0 -0
  28. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.py +0 -0
  29. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.pyi +0 -0
  30. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2_grpc.py +0 -0
  31. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.py +0 -0
  32. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.pyi +0 -0
  33. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2_grpc.py +0 -0
  34. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2.py +0 -0
  35. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2.pyi +0 -0
  36. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2_grpc.py +0 -0
  37. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2.py +0 -0
  38. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2.pyi +0 -0
  39. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2_grpc.py +0 -0
  40. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2.py +0 -0
  41. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2.pyi +0 -0
  42. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2_grpc.py +0 -0
  43. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2.py +0 -0
  44. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2.pyi +0 -0
  45. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2_grpc.py +0 -0
  46. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2.py +0 -0
  47. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2.pyi +0 -0
  48. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2_grpc.py +0 -0
  49. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2.py +0 -0
  50. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2.pyi +0 -0
  51. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2_grpc.py +0 -0
  52. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2.py +0 -0
  53. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2.pyi +0 -0
  54. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2_grpc.py +0 -0
  55. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2.py +0 -0
  56. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2.pyi +0 -0
  57. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2_grpc.py +0 -0
  58. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2.py +0 -0
  59. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2.pyi +0 -0
  60. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2_grpc.py +0 -0
  61. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/py.typed +0 -0
  62. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/unpack_error_status.py +0 -0
  63. {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/v2streams.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kurrentdbclient
3
- Version: 1.3.3
3
+ Version: 1.3.5
4
4
  Summary: Python gRPC Client for KurrentDB
5
5
  License: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
19
20
  Classifier: Programming Language :: Python
20
21
  Classifier: Topic :: Database
21
22
  Classifier: Topic :: Database :: Front-Ends
@@ -56,10 +57,10 @@ with the KurrentDB team, and are officially supported by Kurrent Inc.
56
57
  Although not all aspects of the KurrentDB gRPC API are implemented, most
57
58
  features are presented in an easy-to-use interface.
58
59
 
59
- These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0,
60
- and 26.1, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
60
+ These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0, 26.1,
61
+ and 26.2, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
61
62
  in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12,
62
- 3.13, and 3.14.
63
+ 3.13, 3.14, and 3.15.
63
64
 
64
65
  The test suite has 100% line and branch coverage. The code has typing annotations
65
66
  checked strictly with mypy. The code is formatted with black and isort, and checked
@@ -572,8 +573,8 @@ The table below describes the query string field-values supported by this client
572
573
  | GossipTimeout | integer (default: 5) | The default value (in seconds) of the `timeout` argument of gossip read methods, such as `read_gossip()`. |
573
574
  | MaxDiscoverAttempts | integer (default: 10) | The number of attempts to read gossip when connecting or reconnecting to a cluster member. |
574
575
  | DiscoveryInterval | integer (default: 100) | How long to wait (in milliseconds) between gossip retries. |
575
- | KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_ms" gRPC channel option. |
576
- | KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" gRPC channel option. |
576
+ | KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_time_ms" gRPC channel option. |
577
+ | KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" and "grpc.http2.ping_timeout_ms" gRPC channel options. |
577
578
  | TlsCaFile | string (default: `None`) | Absolute filesystem path to file containing the CA certicate in PEM format. This will be used to verify the server's certificate. |
578
579
  | UserCertFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate in PEM format. |
579
580
  | UserKeyFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate's private key in PEM format. |
@@ -22,10 +22,10 @@ with the KurrentDB team, and are officially supported by Kurrent Inc.
22
22
  Although not all aspects of the KurrentDB gRPC API are implemented, most
23
23
  features are presented in an easy-to-use interface.
24
24
 
25
- These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0,
26
- and 26.1, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
25
+ These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0, 26.1,
26
+ and 26.2, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
27
27
  in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12,
28
- 3.13, and 3.14.
28
+ 3.13, 3.14, and 3.15.
29
29
 
30
30
  The test suite has 100% line and branch coverage. The code has typing annotations
31
31
  checked strictly with mypy. The code is formatted with black and isort, and checked
@@ -538,8 +538,8 @@ The table below describes the query string field-values supported by this client
538
538
  | GossipTimeout | integer (default: 5) | The default value (in seconds) of the `timeout` argument of gossip read methods, such as `read_gossip()`. |
539
539
  | MaxDiscoverAttempts | integer (default: 10) | The number of attempts to read gossip when connecting or reconnecting to a cluster member. |
540
540
  | DiscoveryInterval | integer (default: 100) | How long to wait (in milliseconds) between gossip retries. |
541
- | KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_ms" gRPC channel option. |
542
- | KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" gRPC channel option. |
541
+ | KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_time_ms" gRPC channel option. |
542
+ | KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" and "grpc.http2.ping_timeout_ms" gRPC channel options. |
543
543
  | TlsCaFile | string (default: `None`) | Absolute filesystem path to file containing the CA certicate in PEM format. This will be used to verify the server's certificate. |
544
544
  | UserCertFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate in PEM format. |
545
545
  | UserKeyFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate's private key in PEM format. |
@@ -28,7 +28,7 @@ from kurrentdbclient.streams import (
28
28
  ReadResponse,
29
29
  )
30
30
 
31
- __version__ = "1.3.3"
31
+ __version__ = "1.3.5"
32
32
 
33
33
  __all__ = [
34
34
  "DEFAULT_EXCLUDE_FILTER",
@@ -65,6 +65,7 @@ from kurrentdbclient.exceptions import (
65
65
 
66
66
  if TYPE_CHECKING:
67
67
  from collections.abc import Iterable, Sequence
68
+ from uuid import UUID
68
69
 
69
70
  from kurrentdbclient.persistent import ConsumerStrategy, SubscriptionInfo
70
71
  from kurrentdbclient.projections import ProjectionState, ProjectionStatistics
@@ -80,6 +81,7 @@ def autoreconnect(f: _TCallable) -> _TCallable:
80
81
  client: AsyncKurrentDBClient, *args: Any, **kwargs: Any
81
82
  ) -> Any:
82
83
  await client.connect()
84
+ connection_id = client.connection_id
83
85
  try:
84
86
  return await f(client, *args, **kwargs)
85
87
 
@@ -92,7 +94,8 @@ def autoreconnect(f: _TCallable) -> _TCallable:
92
94
  )
93
95
  ):
94
96
  await client.reconnect(
95
- grpc_target(e.host, e.port) if e.host and e.port else None
97
+ grpc_target(e.host, e.port) if e.host and e.port else None,
98
+ connection_id=connection_id,
96
99
  )
97
100
  await asyncio.sleep(0.1)
98
101
  return await f(client, *args, **kwargs)
@@ -100,13 +103,13 @@ def autoreconnect(f: _TCallable) -> _TCallable:
100
103
 
101
104
  except grpc.aio.UsageError as e:
102
105
  if "Channel is closed" in str(e):
103
- await client.reconnect()
106
+ await client.reconnect(connection_id=connection_id)
104
107
  await asyncio.sleep(0.1)
105
108
  return await f(client, *args, **kwargs)
106
109
  raise # pragma: no cover
107
110
 
108
111
  except ServiceUnavailableError:
109
- await client.reconnect()
112
+ await client.reconnect(connection_id=connection_id)
110
113
  await asyncio.sleep(0.1)
111
114
  return await f(client, *args, **kwargs)
112
115
 
@@ -119,7 +122,7 @@ def retrygrpc(f: _TCallable) -> _TCallable:
119
122
  try:
120
123
  return await f(*args, **kwargs)
121
124
  except GrpcError:
122
- await asyncio.sleep(0.1)
125
+ await asyncio.sleep(0.5)
123
126
  return await f(*args, **kwargs)
124
127
 
125
128
  return cast(_TCallable, retrygrpc_decorator)
@@ -143,6 +146,11 @@ class AsyncKurrentDBClient(BaseKurrentDBClient):
143
146
  self._is_reconnection_required = Event()
144
147
  self._connection_lock = Lock()
145
148
 
149
+ @property
150
+ def connection_id(self) -> UUID:
151
+ assert self._connection is not None
152
+ return self._connection.connection_id
153
+
146
154
  async def connect(self) -> None:
147
155
  if self._connection is None:
148
156
  async with self._connection_lock:
@@ -163,8 +171,12 @@ class AsyncKurrentDBClient(BaseKurrentDBClient):
163
171
  def connection_target(self) -> str:
164
172
  return self.connection.grpc_target
165
173
 
166
- async def reconnect(self, grpc_target: str | None = None) -> None:
167
- self._is_reconnection_required.set()
174
+ async def reconnect(
175
+ self, grpc_target: str | None = None, connection_id: UUID | None = None
176
+ ) -> None:
177
+ # Avoid reconnecting if client already has a new connection.
178
+ if connection_id is None or connection_id == self.connection_id:
179
+ self._is_reconnection_required.set()
168
180
  async with self._connection_lock:
169
181
  if self._is_reconnection_required.is_set():
170
182
  new = await self._connect(grpc_target)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import json
4
5
  import random
5
6
  import sys
@@ -62,6 +63,7 @@ from kurrentdbclient.events import (
62
63
  StreamStateCheck,
63
64
  )
64
65
  from kurrentdbclient.exceptions import (
66
+ AccessDeniedError,
65
67
  DiscoveryFailedError,
66
68
  FollowerNotFoundError,
67
69
  GrpcError,
@@ -81,6 +83,7 @@ from kurrentdbclient.gossip import (
81
83
 
82
84
  if TYPE_CHECKING:
83
85
  from collections.abc import Iterable, Sequence
86
+ from uuid import UUID
84
87
 
85
88
  from kurrentdbclient.persistent import (
86
89
  ConsumerStrategy,
@@ -116,9 +119,14 @@ def autoreconnect(f: _TCallable) -> _TCallable:
116
119
  def autoreconnect_decorator(*args: Any, **kwargs: Any) -> Any:
117
120
  client = args[0]
118
121
  assert isinstance(client, KurrentDBClient)
122
+ connection_id = client.connection_id
119
123
  try:
120
124
  return f(*args, **kwargs)
121
125
 
126
+ # TODO: Maybe refine the retry so that we do reconnect and try again when
127
+ # there is connection failure after retrying without reconnecting because
128
+ # the connection has changed. Need to think about this a bit more...
129
+
122
130
  except NodeIsNotLeaderError as e:
123
131
  if (
124
132
  client.connection_spec.options.node_preference == NODE_PREFERENCE_LEADER
@@ -128,7 +136,8 @@ def autoreconnect(f: _TCallable) -> _TCallable:
128
136
  )
129
137
  ):
130
138
  client.reconnect(
131
- grpc_target(e.host, e.port) if e.host and e.port else None
139
+ grpc_target(e.host, e.port) if e.host and e.port else None,
140
+ connection_id=connection_id,
132
141
  )
133
142
  sleep(0.1)
134
143
  return f(*args, **kwargs)
@@ -137,13 +146,13 @@ def autoreconnect(f: _TCallable) -> _TCallable:
137
146
  except ValueError as e:
138
147
  s = str(e)
139
148
  if "Channel closed!" in s or "Cannot invoke RPC on closed channel!" in s:
140
- client.reconnect()
149
+ client.reconnect(connection_id=connection_id)
141
150
  sleep(0.1)
142
151
  return f(*args, **kwargs)
143
152
  raise # pragma: no cover
144
153
 
145
154
  except ServiceUnavailableError:
146
- client.reconnect()
155
+ client.reconnect(connection_id=connection_id)
147
156
  sleep(0.1)
148
157
  return f(*args, **kwargs)
149
158
 
@@ -208,12 +217,16 @@ class BaseKurrentDBClient(ABC):
208
217
 
209
218
  self._default_deadline = self.connection_spec.options.default_deadline
210
219
 
220
+ # GRPC options ref: https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
211
221
  self.grpc_options: GrpcOptions = (
212
222
  ("grpc.max_receive_message_length", 17 * 1024 * 1024),
213
223
  )
214
224
  if self.connection_spec.options.keep_alive_interval is not None:
215
225
  self.grpc_options += (
216
- ("grpc.keepalive_ms", self.connection_spec.options.keep_alive_interval),
226
+ (
227
+ "grpc.keepalive_time_ms",
228
+ self.connection_spec.options.keep_alive_interval,
229
+ ),
217
230
  )
218
231
  if self.connection_spec.options.keep_alive_timeout is not None:
219
232
  self.grpc_options += (
@@ -221,6 +234,12 @@ class BaseKurrentDBClient(ABC):
221
234
  "grpc.keepalive_timeout_ms",
222
235
  self.connection_spec.options.keep_alive_timeout,
223
236
  ),
237
+ # gRPC closes a connection whose ping is not answered after
238
+ # this time (default 60s), not after keepalive_timeout_ms.
239
+ (
240
+ "grpc.http2.ping_timeout_ms",
241
+ self.connection_spec.options.keep_alive_timeout,
242
+ ),
224
243
  )
225
244
 
226
245
  self._call_metadata = (
@@ -312,6 +331,10 @@ class KurrentDBClient(BaseKurrentDBClient):
312
331
  def connection_target(self) -> str:
313
332
  return self._connection.grpc_target
314
333
 
334
+ @property
335
+ def connection_id(self) -> UUID:
336
+ return self._connection.connection_id
337
+
315
338
  @property
316
339
  def streams(self) -> StreamsService:
317
340
  return self._connection.streams
@@ -401,14 +424,22 @@ class KurrentDBClient(BaseKurrentDBClient):
401
424
 
402
425
  return connection
403
426
 
404
- def reconnect(self, grpc_target: str | None = None) -> None:
405
- self._is_reconnection_required.set()
427
+ def reconnect(
428
+ self, grpc_target: str | None = None, *, connection_id: UUID | None = None
429
+ ) -> None:
430
+ # Avoid reconnecting if client already has a new connection.
431
+ if connection_id is None or connection_id == self.connection_id:
432
+ self._is_reconnection_required.set()
433
+ # Single reconnection.
406
434
  with self._reconnection_lock:
407
435
  if self._is_reconnection_required.is_set():
408
436
  new_conn = self._connect(grpc_target)
409
437
  old_conn, self._connection = self._connection, new_conn
410
438
  old_conn.close()
411
439
  self._is_reconnection_required.clear()
440
+ # Exercise the connection (avoids delayed errors due to lazy channel)
441
+ with contextlib.suppress(AccessDeniedError):
442
+ self._get_commit_position()
412
443
  else: # pragma: no cover
413
444
  # Todo: Test with concurrent writes to wrong node state.
414
445
  pass
@@ -668,17 +699,19 @@ class KurrentDBClient(BaseKurrentDBClient):
668
699
  """
669
700
  Returns a sequence of recorded events from the named stream.
670
701
  """
671
- with self.read_stream(
702
+ with self._read_stream(
672
703
  stream_name=stream_name,
673
704
  stream_position=stream_position,
674
705
  backwards=backwards,
675
706
  resolve_links=resolve_links,
676
707
  limit=limit,
677
708
  timeout=timeout,
678
- credentials=credentials or self._call_credentials,
709
+ credentials=credentials,
679
710
  ) as events:
680
711
  return tuple(events)
681
712
 
713
+ @retrygrpc
714
+ @autoreconnect
682
715
  def read_stream(
683
716
  self,
684
717
  stream_name: str,
@@ -689,6 +722,30 @@ class KurrentDBClient(BaseKurrentDBClient):
689
722
  limit: int = sys.maxsize,
690
723
  timeout: float | None = None,
691
724
  credentials: grpc.CallCredentials | None = None,
725
+ ) -> AbstractReadResponse:
726
+ """
727
+ Reads recorded events from the named stream.
728
+ """
729
+ return self._read_stream(
730
+ stream_name=stream_name,
731
+ stream_position=stream_position,
732
+ backwards=backwards,
733
+ resolve_links=resolve_links,
734
+ limit=limit,
735
+ timeout=timeout,
736
+ credentials=credentials,
737
+ )
738
+
739
+ def _read_stream(
740
+ self,
741
+ stream_name: str,
742
+ *,
743
+ stream_position: int | None = None,
744
+ backwards: bool = False,
745
+ resolve_links: bool = False,
746
+ limit: int = sys.maxsize,
747
+ timeout: float | None = None,
748
+ credentials: grpc.CallCredentials | None = None,
692
749
  ) -> AbstractReadResponse:
693
750
  """
694
751
  Reads recorded events from the named stream.
@@ -704,6 +761,8 @@ class KurrentDBClient(BaseKurrentDBClient):
704
761
  credentials=credentials or self._call_credentials,
705
762
  )
706
763
 
764
+ @retrygrpc
765
+ @autoreconnect
707
766
  def read_all(
708
767
  self,
709
768
  *,
@@ -717,6 +776,36 @@ class KurrentDBClient(BaseKurrentDBClient):
717
776
  limit: int = sys.maxsize,
718
777
  timeout: float | None = None,
719
778
  credentials: grpc.CallCredentials | None = None,
779
+ ) -> AbstractReadResponse:
780
+ """
781
+ Reads recorded events in "all streams" in the database.
782
+ """
783
+ return self._read_all(
784
+ commit_position=commit_position,
785
+ backwards=backwards,
786
+ resolve_links=resolve_links,
787
+ filter_exclude=filter_exclude,
788
+ filter_include=filter_include,
789
+ filter_by_stream_name=filter_by_stream_name,
790
+ filter_by_prefix=filter_by_prefix,
791
+ limit=limit,
792
+ timeout=timeout,
793
+ credentials=credentials,
794
+ )
795
+
796
+ def _read_all(
797
+ self,
798
+ *,
799
+ commit_position: int | None = None,
800
+ backwards: bool = False,
801
+ resolve_links: bool = False,
802
+ filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
803
+ filter_include: Sequence[str] = (),
804
+ filter_by_stream_name: bool = False,
805
+ filter_by_prefix: bool = False,
806
+ limit: int = sys.maxsize,
807
+ timeout: float | None = None,
808
+ credentials: grpc.CallCredentials | None = None,
720
809
  ) -> AbstractReadResponse:
721
810
  """
722
811
  Reads recorded events in "all streams" in the database.
@@ -735,6 +824,8 @@ class KurrentDBClient(BaseKurrentDBClient):
735
824
  credentials=credentials or self._call_credentials,
736
825
  )
737
826
 
827
+ @retrygrpc
828
+ @autoreconnect
738
829
  def read_index(
739
830
  self,
740
831
  index_name: str,
@@ -748,7 +839,7 @@ class KurrentDBClient(BaseKurrentDBClient):
748
839
  if not index_name.startswith("$idx-"):
749
840
  index_name = f"$idx-{index_name}"
750
841
 
751
- return self.read_all(
842
+ return self._read_all(
752
843
  commit_position=commit_position,
753
844
  filter_include=(index_name,),
754
845
  filter_by_stream_name=True,
@@ -807,15 +898,34 @@ class KurrentDBClient(BaseKurrentDBClient):
807
898
  """
808
899
  Returns the current commit position of the database.
809
900
  """
810
- read_response = self.streams.read(
901
+ return self._get_commit_position(
902
+ filter_exclude=filter_exclude,
903
+ filter_include=filter_include,
904
+ filter_by_stream_name=filter_by_stream_name,
905
+ timeout=timeout,
906
+ credentials=credentials,
907
+ )
908
+
909
+ def _get_commit_position(
910
+ self,
911
+ *,
912
+ filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
913
+ filter_include: Sequence[str] = (),
914
+ filter_by_stream_name: bool = False,
915
+ timeout: float | None = None,
916
+ credentials: grpc.CallCredentials | None = None,
917
+ ) -> int:
918
+ """
919
+ Returns the current commit position of the database.
920
+ """
921
+ read_response = self._read_all(
811
922
  backwards=True,
812
923
  filter_exclude=filter_exclude,
813
924
  filter_include=filter_include,
814
925
  filter_by_stream_name=filter_by_stream_name,
815
926
  limit=1,
816
927
  timeout=timeout,
817
- metadata=self._call_metadata,
818
- credentials=credentials or self._call_credentials,
928
+ credentials=credentials,
819
929
  )
820
930
  recorded_events = tuple(read_response)
821
931
  if recorded_events:
@@ -895,6 +1005,45 @@ class KurrentDBClient(BaseKurrentDBClient):
895
1005
  include_fell_behind: bool = False,
896
1006
  timeout: float | None = None,
897
1007
  credentials: grpc.CallCredentials | None = None,
1008
+ ) -> AbstractCatchupSubscription:
1009
+ """
1010
+ Starts a catch-up subscription, from which all
1011
+ recorded events in the database can be received.
1012
+ """
1013
+ return self._subscribe_to_all(
1014
+ commit_position=commit_position,
1015
+ from_end=from_end,
1016
+ resolve_links=resolve_links,
1017
+ filter_exclude=filter_exclude,
1018
+ filter_include=filter_include,
1019
+ filter_by_stream_name=filter_by_stream_name,
1020
+ filter_by_prefix=filter_by_prefix,
1021
+ include_checkpoints=include_checkpoints,
1022
+ window_size=window_size,
1023
+ checkpoint_interval_multiplier=checkpoint_interval_multiplier,
1024
+ include_caught_up=include_caught_up,
1025
+ include_fell_behind=include_fell_behind,
1026
+ timeout=timeout,
1027
+ credentials=credentials,
1028
+ )
1029
+
1030
+ def _subscribe_to_all(
1031
+ self,
1032
+ *,
1033
+ commit_position: int | None = None,
1034
+ from_end: bool = False,
1035
+ resolve_links: bool = False,
1036
+ filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
1037
+ filter_include: Sequence[str] = (),
1038
+ filter_by_stream_name: bool = False,
1039
+ filter_by_prefix: bool = False,
1040
+ include_checkpoints: bool = False,
1041
+ window_size: int = DEFAULT_WINDOW_SIZE,
1042
+ checkpoint_interval_multiplier: int = DEFAULT_CHECKPOINT_INTERVAL_MULTIPLIER,
1043
+ include_caught_up: bool = False,
1044
+ include_fell_behind: bool = False,
1045
+ timeout: float | None = None,
1046
+ credentials: grpc.CallCredentials | None = None,
898
1047
  ) -> AbstractCatchupSubscription:
899
1048
  """
900
1049
  Starts a catch-up subscription, from which all
@@ -919,6 +1068,8 @@ class KurrentDBClient(BaseKurrentDBClient):
919
1068
  credentials=credentials or self._call_credentials,
920
1069
  )
921
1070
 
1071
+ @retrygrpc
1072
+ @autoreconnect
922
1073
  def subscribe_to_index(
923
1074
  self,
924
1075
  index_name: str,
@@ -931,7 +1082,7 @@ class KurrentDBClient(BaseKurrentDBClient):
931
1082
  if not index_name.startswith("$idx-"):
932
1083
  index_name = f"$idx-{index_name}"
933
1084
 
934
- return self.subscribe_to_all(
1085
+ return self._subscribe_to_all(
935
1086
  commit_position=commit_position,
936
1087
  filter_include=(index_name,),
937
1088
  filter_by_stream_name=True,
@@ -1411,6 +1562,24 @@ class KurrentDBClient(BaseKurrentDBClient):
1411
1562
  *,
1412
1563
  timeout: float | None = None,
1413
1564
  credentials: grpc.CallCredentials | None = None,
1565
+ ) -> SubscriptionInfo:
1566
+ """
1567
+ Gets info for a persistent subscription.
1568
+ """
1569
+ return self._get_subscription_info(
1570
+ group_name=group_name,
1571
+ stream_name=stream_name,
1572
+ timeout=timeout,
1573
+ credentials=credentials,
1574
+ )
1575
+
1576
+ def _get_subscription_info(
1577
+ self,
1578
+ group_name: str,
1579
+ stream_name: str | None = None,
1580
+ *,
1581
+ timeout: float | None = None,
1582
+ credentials: grpc.CallCredentials | None = None,
1414
1583
  ) -> SubscriptionInfo:
1415
1584
  """
1416
1585
  Gets info for a persistent subscription.
@@ -1744,7 +1913,7 @@ class KurrentDBClient(BaseKurrentDBClient):
1744
1913
  Updates a persistent subscription on one stream.
1745
1914
  """
1746
1915
 
1747
- info = self.get_subscription_info(
1916
+ info = self._get_subscription_info(
1748
1917
  group_name=group_name,
1749
1918
  stream_name=stream_name,
1750
1919
  timeout=timeout,