kurrentdbclient 1.3.4__tar.gz → 1.3.6__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.
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/PKG-INFO +3 -3
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/README.md +2 -2
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/__init__.py +1 -1
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/asyncio_client.py +18 -6
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/client.py +172 -13
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/common.py +121 -27
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/connection.py +2 -2
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/exceptions.py +8 -2
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/spanners.py +6 -10
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/persistent.py +27 -10
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/streams.py +46 -14
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/pyproject.toml +2 -2
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/LICENSE +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/connection_spec.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/events.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/gossip.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/__init__.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/__init__.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/attributes.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/package.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/utils.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/instrumentation/opentelemetry/version.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/projections.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/errors_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/cluster_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/cluster_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/cluster_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/gossip_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/gossip_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/gossip_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/persistent_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/persistent_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/persistent_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/shared_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/shared_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/shared_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/status_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/status_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/status_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/streams_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/streams_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/streams_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/streams_pb2.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/streams_pb2.pyi +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/streams_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/py.typed +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/unpack_error_status.py +0 -0
- {kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/v2streams.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kurrentdbclient
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: Python gRPC Client for KurrentDB
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
@@ -57,8 +57,8 @@ with the KurrentDB team, and are officially supported by Kurrent Inc.
|
|
|
57
57
|
Although not all aspects of the KurrentDB gRPC API are implemented, most
|
|
58
58
|
features are presented in an easy-to-use interface.
|
|
59
59
|
|
|
60
|
-
These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0,
|
|
61
|
-
and 26.
|
|
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,
|
|
62
62
|
in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12,
|
|
63
63
|
3.13, 3.14, and 3.15.
|
|
64
64
|
|
|
@@ -22,8 +22,8 @@ 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.
|
|
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
28
|
3.13, 3.14, and 3.15.
|
|
29
29
|
|
|
@@ -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.
|
|
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(
|
|
167
|
-
self
|
|
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
|
|
|
@@ -322,6 +331,10 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
322
331
|
def connection_target(self) -> str:
|
|
323
332
|
return self._connection.grpc_target
|
|
324
333
|
|
|
334
|
+
@property
|
|
335
|
+
def connection_id(self) -> UUID:
|
|
336
|
+
return self._connection.connection_id
|
|
337
|
+
|
|
325
338
|
@property
|
|
326
339
|
def streams(self) -> StreamsService:
|
|
327
340
|
return self._connection.streams
|
|
@@ -411,14 +424,22 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
411
424
|
|
|
412
425
|
return connection
|
|
413
426
|
|
|
414
|
-
def reconnect(
|
|
415
|
-
self
|
|
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.
|
|
416
434
|
with self._reconnection_lock:
|
|
417
435
|
if self._is_reconnection_required.is_set():
|
|
418
436
|
new_conn = self._connect(grpc_target)
|
|
419
437
|
old_conn, self._connection = self._connection, new_conn
|
|
420
438
|
old_conn.close()
|
|
421
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()
|
|
422
443
|
else: # pragma: no cover
|
|
423
444
|
# Todo: Test with concurrent writes to wrong node state.
|
|
424
445
|
pass
|
|
@@ -678,17 +699,19 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
678
699
|
"""
|
|
679
700
|
Returns a sequence of recorded events from the named stream.
|
|
680
701
|
"""
|
|
681
|
-
with self.
|
|
702
|
+
with self._read_stream(
|
|
682
703
|
stream_name=stream_name,
|
|
683
704
|
stream_position=stream_position,
|
|
684
705
|
backwards=backwards,
|
|
685
706
|
resolve_links=resolve_links,
|
|
686
707
|
limit=limit,
|
|
687
708
|
timeout=timeout,
|
|
688
|
-
credentials=credentials
|
|
709
|
+
credentials=credentials,
|
|
689
710
|
) as events:
|
|
690
711
|
return tuple(events)
|
|
691
712
|
|
|
713
|
+
@retrygrpc
|
|
714
|
+
@autoreconnect
|
|
692
715
|
def read_stream(
|
|
693
716
|
self,
|
|
694
717
|
stream_name: str,
|
|
@@ -699,6 +722,30 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
699
722
|
limit: int = sys.maxsize,
|
|
700
723
|
timeout: float | None = None,
|
|
701
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,
|
|
702
749
|
) -> AbstractReadResponse:
|
|
703
750
|
"""
|
|
704
751
|
Reads recorded events from the named stream.
|
|
@@ -714,6 +761,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
714
761
|
credentials=credentials or self._call_credentials,
|
|
715
762
|
)
|
|
716
763
|
|
|
764
|
+
@retrygrpc
|
|
765
|
+
@autoreconnect
|
|
717
766
|
def read_all(
|
|
718
767
|
self,
|
|
719
768
|
*,
|
|
@@ -727,6 +776,36 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
727
776
|
limit: int = sys.maxsize,
|
|
728
777
|
timeout: float | None = None,
|
|
729
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,
|
|
730
809
|
) -> AbstractReadResponse:
|
|
731
810
|
"""
|
|
732
811
|
Reads recorded events in "all streams" in the database.
|
|
@@ -745,6 +824,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
745
824
|
credentials=credentials or self._call_credentials,
|
|
746
825
|
)
|
|
747
826
|
|
|
827
|
+
@retrygrpc
|
|
828
|
+
@autoreconnect
|
|
748
829
|
def read_index(
|
|
749
830
|
self,
|
|
750
831
|
index_name: str,
|
|
@@ -758,7 +839,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
758
839
|
if not index_name.startswith("$idx-"):
|
|
759
840
|
index_name = f"$idx-{index_name}"
|
|
760
841
|
|
|
761
|
-
return self.
|
|
842
|
+
return self._read_all(
|
|
762
843
|
commit_position=commit_position,
|
|
763
844
|
filter_include=(index_name,),
|
|
764
845
|
filter_by_stream_name=True,
|
|
@@ -817,15 +898,34 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
817
898
|
"""
|
|
818
899
|
Returns the current commit position of the database.
|
|
819
900
|
"""
|
|
820
|
-
|
|
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(
|
|
821
922
|
backwards=True,
|
|
822
923
|
filter_exclude=filter_exclude,
|
|
823
924
|
filter_include=filter_include,
|
|
824
925
|
filter_by_stream_name=filter_by_stream_name,
|
|
825
926
|
limit=1,
|
|
826
927
|
timeout=timeout,
|
|
827
|
-
|
|
828
|
-
credentials=credentials or self._call_credentials,
|
|
928
|
+
credentials=credentials,
|
|
829
929
|
)
|
|
830
930
|
recorded_events = tuple(read_response)
|
|
831
931
|
if recorded_events:
|
|
@@ -905,6 +1005,45 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
905
1005
|
include_fell_behind: bool = False,
|
|
906
1006
|
timeout: float | None = None,
|
|
907
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,
|
|
908
1047
|
) -> AbstractCatchupSubscription:
|
|
909
1048
|
"""
|
|
910
1049
|
Starts a catch-up subscription, from which all
|
|
@@ -929,6 +1068,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
929
1068
|
credentials=credentials or self._call_credentials,
|
|
930
1069
|
)
|
|
931
1070
|
|
|
1071
|
+
@retrygrpc
|
|
1072
|
+
@autoreconnect
|
|
932
1073
|
def subscribe_to_index(
|
|
933
1074
|
self,
|
|
934
1075
|
index_name: str,
|
|
@@ -941,7 +1082,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
941
1082
|
if not index_name.startswith("$idx-"):
|
|
942
1083
|
index_name = f"$idx-{index_name}"
|
|
943
1084
|
|
|
944
|
-
return self.
|
|
1085
|
+
return self._subscribe_to_all(
|
|
945
1086
|
commit_position=commit_position,
|
|
946
1087
|
filter_include=(index_name,),
|
|
947
1088
|
filter_by_stream_name=True,
|
|
@@ -1421,6 +1562,24 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
1421
1562
|
*,
|
|
1422
1563
|
timeout: float | None = None,
|
|
1423
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,
|
|
1424
1583
|
) -> SubscriptionInfo:
|
|
1425
1584
|
"""
|
|
1426
1585
|
Gets info for a persistent subscription.
|
|
@@ -1754,7 +1913,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
1754
1913
|
Updates a persistent subscription on one stream.
|
|
1755
1914
|
"""
|
|
1756
1915
|
|
|
1757
|
-
info = self.
|
|
1916
|
+
info = self._get_subscription_info(
|
|
1758
1917
|
group_name=group_name,
|
|
1759
1918
|
stream_name=stream_name,
|
|
1760
1919
|
timeout=timeout,
|
|
@@ -30,8 +30,10 @@ import kurrentdbclient.protos.v2.streams.errors_pb2 as v2streams_errors_pb2
|
|
|
30
30
|
from kurrentdbclient.events import RecordedEvent
|
|
31
31
|
from kurrentdbclient.exceptions import (
|
|
32
32
|
AbortedByServerError,
|
|
33
|
+
AccessDeniedError,
|
|
33
34
|
AlreadyExistsError,
|
|
34
35
|
CancelledByClientError,
|
|
36
|
+
CancelledError,
|
|
35
37
|
ConsistencyCheckFailure,
|
|
36
38
|
ConsistencyChecksFailedError,
|
|
37
39
|
ConsumerTooSlowError,
|
|
@@ -109,13 +111,23 @@ class BaseGrpcStreamer:
|
|
|
109
111
|
TGrpcStreamer = TypeVar("TGrpcStreamer", bound=BaseGrpcStreamer)
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
class GrpcStreamersCollectionClosedError(Exception):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
112
118
|
class BaseGrpcStreamers(Generic[TGrpcStreamer]):
|
|
113
119
|
def __init__(self) -> None:
|
|
114
120
|
self.map: WeakValueDictionary[int, TGrpcStreamer] = WeakValueDictionary()
|
|
115
121
|
self.lock = threading.Lock()
|
|
122
|
+
self._is_closed = False
|
|
116
123
|
|
|
117
124
|
def add(self, streamer: TGrpcStreamer) -> None:
|
|
125
|
+
# print(f"Adding streamer: {id(streamer)}")
|
|
118
126
|
with self.lock:
|
|
127
|
+
if self._is_closed:
|
|
128
|
+
# This is hit by the test_replicate_github_issue_40 tests when
|
|
129
|
+
# run separately but for some reason not with the full suite
|
|
130
|
+
raise GrpcStreamersCollectionClosedError
|
|
119
131
|
self.map[id(streamer)] = streamer
|
|
120
132
|
|
|
121
133
|
def __iter__(self) -> Iterator[TGrpcStreamer]:
|
|
@@ -132,24 +144,42 @@ TGrpcStreamers = TypeVar("TGrpcStreamers", bound=BaseGrpcStreamers[Any])
|
|
|
132
144
|
|
|
133
145
|
class GrpcStreamer(BaseGrpcStreamer, ABC):
|
|
134
146
|
def __init__(self, grpc_streamers: GrpcStreamers) -> None:
|
|
135
|
-
self._grpc_streamers = grpc_streamers
|
|
136
|
-
self._grpc_streamers.add(self)
|
|
137
147
|
self._is_stopped = False
|
|
148
|
+
self._is_cancelled = False
|
|
138
149
|
self._is_stopped_lock = threading.Lock()
|
|
150
|
+
self._grpc_streamers = grpc_streamers
|
|
151
|
+
try:
|
|
152
|
+
self._grpc_streamers.add(self)
|
|
153
|
+
except GrpcStreamersCollectionClosedError: # pragma: no cover
|
|
154
|
+
# This is hit by the test_replicate_github_issue_40 tests when
|
|
155
|
+
# run separately but for some reason not with the full suite
|
|
156
|
+
self.stop(cancel=True)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_stopped(self) -> bool:
|
|
160
|
+
return self._is_stopped
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def is_cancelled(self) -> bool:
|
|
164
|
+
return self._is_cancelled
|
|
139
165
|
|
|
140
166
|
@abstractmethod
|
|
141
|
-
def stop(self) -> None:
|
|
167
|
+
def stop(self, *, cancel: bool = False) -> None:
|
|
142
168
|
"""
|
|
143
169
|
Stops the iterator(s) of streaming call.
|
|
144
170
|
"""
|
|
145
171
|
|
|
146
|
-
def
|
|
172
|
+
def _remove(self) -> None:
|
|
173
|
+
self._grpc_streamers.remove(self)
|
|
174
|
+
|
|
175
|
+
def _set_is_stopped(self, *, cancel: bool = False) -> bool:
|
|
147
176
|
is_stopped = True
|
|
148
177
|
if self._is_stopped is False:
|
|
149
178
|
with self._is_stopped_lock:
|
|
150
179
|
if self._is_stopped is False:
|
|
151
180
|
is_stopped = False
|
|
152
181
|
self._is_stopped = True
|
|
182
|
+
self._is_cancelled = cancel
|
|
153
183
|
else: # pragma: no cover
|
|
154
184
|
pass
|
|
155
185
|
return is_stopped
|
|
@@ -157,32 +187,57 @@ class GrpcStreamer(BaseGrpcStreamer, ABC):
|
|
|
157
187
|
|
|
158
188
|
class GrpcStreamers(BaseGrpcStreamers[GrpcStreamer]):
|
|
159
189
|
def close(self) -> None:
|
|
190
|
+
with self.lock:
|
|
191
|
+
self._is_closed = True
|
|
160
192
|
for grpc_streamer in self:
|
|
161
|
-
|
|
162
|
-
grpc_streamer.stop()
|
|
163
|
-
# print("closed streamer")
|
|
193
|
+
grpc_streamer.stop(cancel=True)
|
|
164
194
|
|
|
165
195
|
|
|
166
196
|
class AsyncGrpcStreamer(BaseGrpcStreamer, ABC):
|
|
167
197
|
def __init__(self, grpc_streamers: AsyncGrpcStreamers) -> None:
|
|
168
198
|
self._grpc_streamers = grpc_streamers
|
|
169
|
-
self._grpc_streamers.add(self)
|
|
170
199
|
self._is_stopped = False
|
|
200
|
+
self._is_cancelled = False
|
|
171
201
|
self._is_stopped_lock = asyncio.Lock()
|
|
172
202
|
|
|
203
|
+
async def add_to_steamers(self) -> None:
|
|
204
|
+
try:
|
|
205
|
+
self._grpc_streamers.add(self)
|
|
206
|
+
except GrpcStreamersCollectionClosedError: # pragma: no cover
|
|
207
|
+
# This is hit by the test_replicate_github_issue_40 tests when
|
|
208
|
+
# run separately but for some reason not with the full suite
|
|
209
|
+
await self.stop(cancel=True)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def is_stopped(self) -> bool:
|
|
213
|
+
return self._is_stopped
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def is_cancelled(self) -> bool:
|
|
217
|
+
return self._is_cancelled
|
|
218
|
+
|
|
173
219
|
@abstractmethod
|
|
174
|
-
async def stop(
|
|
220
|
+
async def stop(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
timeout: float | None = None,
|
|
224
|
+
cancel: bool = False,
|
|
225
|
+
) -> None:
|
|
175
226
|
"""
|
|
176
227
|
Stops the iterator(s) of streaming call.
|
|
177
228
|
"""
|
|
178
229
|
|
|
179
|
-
|
|
230
|
+
def _remove(self) -> None:
|
|
231
|
+
self._grpc_streamers.remove(self)
|
|
232
|
+
|
|
233
|
+
async def _set_is_stopped(self, *, cancel: bool = False) -> bool:
|
|
180
234
|
is_stopped = True
|
|
181
235
|
if self._is_stopped is False:
|
|
182
236
|
async with self._is_stopped_lock:
|
|
183
237
|
if self._is_stopped is False:
|
|
184
238
|
is_stopped = False
|
|
185
239
|
self._is_stopped = True
|
|
240
|
+
self._is_cancelled = cancel
|
|
186
241
|
else: # pragma: no cover
|
|
187
242
|
pass
|
|
188
243
|
return is_stopped
|
|
@@ -190,9 +245,11 @@ class AsyncGrpcStreamer(BaseGrpcStreamer, ABC):
|
|
|
190
245
|
|
|
191
246
|
class AsyncGrpcStreamers(BaseGrpcStreamers[AsyncGrpcStreamer]):
|
|
192
247
|
async def close(self) -> None:
|
|
248
|
+
with self.lock:
|
|
249
|
+
self._is_closed = True
|
|
193
250
|
for async_grpc_streamer in self:
|
|
194
251
|
# print("closing streamer")
|
|
195
|
-
await async_grpc_streamer.stop(timeout=1)
|
|
252
|
+
await async_grpc_streamer.stop(timeout=1, cancel=True)
|
|
196
253
|
# print("closed streamer")
|
|
197
254
|
|
|
198
255
|
|
|
@@ -295,65 +352,93 @@ def handle_rpc_error(e: grpc.RpcError) -> KurrentDBClientError: # noqa: PLR0911
|
|
|
295
352
|
)
|
|
296
353
|
return KurrentDBClientError(details_str)
|
|
297
354
|
|
|
355
|
+
# Handle error according the status code of the given error.
|
|
298
356
|
if e.code() == grpc.StatusCode.UNKNOWN:
|
|
299
357
|
if "Exception was thrown by handler" in details_str:
|
|
358
|
+
# This just happens from time to time in KurrentDB (various operations)
|
|
300
359
|
return ExceptionThrownByHandlerError(details_str)
|
|
360
|
+
|
|
361
|
+
# no cover >=26.2: start
|
|
301
362
|
if (
|
|
302
363
|
"Envelope callback expected Updated, received Conflict instead"
|
|
303
364
|
in details_str
|
|
304
365
|
):
|
|
305
|
-
# Projections.Create
|
|
366
|
+
# Projections.Create in when projection exists with same name.
|
|
367
|
+
# KurrentDB >=26.2 returns error with status code ALREADY_EXISTS.
|
|
306
368
|
return AlreadyExistsError(details_str)
|
|
369
|
+
# no cover >=26.2: stop
|
|
370
|
+
|
|
371
|
+
# no cover >=24.10: start
|
|
307
372
|
if (
|
|
308
373
|
"Envelope callback expected Updated, received NotFound instead"
|
|
309
374
|
in details_str
|
|
310
375
|
):
|
|
311
376
|
# Projections.Update and Projections.Delete does this in < v24.6
|
|
312
|
-
return NotFoundError(details_str)
|
|
377
|
+
return NotFoundError(details_str)
|
|
378
|
+
# no cover >=24.10: stop
|
|
379
|
+
|
|
380
|
+
# no cover >=24.10: start
|
|
313
381
|
if (
|
|
314
382
|
"Envelope callback expected Statistics, received NotFound instead"
|
|
315
383
|
in details_str
|
|
316
384
|
):
|
|
317
385
|
# Projections.Statistics does this in < v24.6
|
|
318
|
-
return NotFoundError(details_str)
|
|
386
|
+
return NotFoundError(details_str)
|
|
387
|
+
# no cover >=24.10: stop
|
|
388
|
+
|
|
389
|
+
# no cover >=24.10: start
|
|
319
390
|
if (
|
|
320
391
|
"Envelope callback expected ProjectionState, received NotFound instead"
|
|
321
392
|
in details_str
|
|
322
393
|
):
|
|
323
394
|
# Projections.State does this in < v24.6
|
|
324
|
-
return NotFoundError(details_str)
|
|
395
|
+
return NotFoundError(details_str)
|
|
396
|
+
# no cover >=24.10: stop
|
|
397
|
+
|
|
398
|
+
# no cover >=24.10: start
|
|
325
399
|
if (
|
|
326
400
|
"Envelope callback expected ProjectionResult, received NotFound instead"
|
|
327
401
|
in details_str
|
|
328
402
|
):
|
|
329
403
|
# Projections.Result does this in < v24.6
|
|
330
404
|
return NotFoundError(details_str) # pragma: no cover
|
|
405
|
+
# no cover >=24.10: stop
|
|
406
|
+
|
|
407
|
+
# no cover >=26.2: start
|
|
331
408
|
if (
|
|
332
409
|
"Envelope callback expected Updated, received OperationFailed instead"
|
|
333
410
|
in details_str
|
|
334
411
|
):
|
|
335
|
-
# Projections.Delete does this
|
|
412
|
+
# Projections.Delete does this when projection not stopped.
|
|
413
|
+
# KurrentDB >=26.2 returns error with status code FAILED_PRECONDITION.
|
|
336
414
|
return OperationFailedError(details_str)
|
|
415
|
+
# no cover >=26.2: stop
|
|
416
|
+
|
|
337
417
|
# no cover <25.1: start
|
|
338
418
|
if "Unexpected FilteredReadAllResult: InvalidPosition" in details_str:
|
|
339
419
|
# Commit position does not exist.
|
|
340
420
|
return InvalidCommitPositionError(details_str)
|
|
341
421
|
# no cover <25.1: stop
|
|
342
|
-
|
|
422
|
+
|
|
423
|
+
# Catch all other unknown errors.
|
|
424
|
+
return UnknownError(details_str)
|
|
343
425
|
|
|
344
426
|
if e.code() == grpc.StatusCode.ABORTED:
|
|
345
427
|
if "Consumer too slow" in details_str:
|
|
346
428
|
return ConsumerTooSlowError(details_str)
|
|
347
429
|
return AbortedByServerError(details_str)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return
|
|
430
|
+
|
|
431
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
432
|
+
if details_str == "Locally cancelled by application!":
|
|
433
|
+
return CancelledByClientError(details_str)
|
|
434
|
+
return CancelledError(details_str)
|
|
435
|
+
|
|
353
436
|
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
354
437
|
return GrpcDeadlineExceededError(details_str)
|
|
438
|
+
|
|
355
439
|
if e.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
356
440
|
return UnauthenticatedError(details_str)
|
|
441
|
+
|
|
357
442
|
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
358
443
|
if "SSL_ERROR" in details_str:
|
|
359
444
|
# root_certificates is None and CA cert not installed
|
|
@@ -365,8 +450,10 @@ def handle_rpc_error(e: grpc.RpcError) -> KurrentDBClientError: # noqa: PLR0911
|
|
|
365
450
|
# given root_certificates is invalid
|
|
366
451
|
return SSLError(details_str)
|
|
367
452
|
return ServiceUnavailableError(details_str)
|
|
453
|
+
|
|
368
454
|
if e.code() == grpc.StatusCode.ALREADY_EXISTS:
|
|
369
455
|
return AlreadyExistsError(details_str)
|
|
456
|
+
|
|
370
457
|
if e.code() == grpc.StatusCode.NOT_FOUND:
|
|
371
458
|
if details_str == "Leader info available":
|
|
372
459
|
trailing_metadata = {m[0]: m[1] for m in e.trailing_metadata()} # type: ignore[index]
|
|
@@ -382,17 +469,24 @@ def handle_rpc_error(e: grpc.RpcError) -> KurrentDBClientError: # noqa: PLR0911
|
|
|
382
469
|
),
|
|
383
470
|
)
|
|
384
471
|
return NotFoundError()
|
|
472
|
+
|
|
385
473
|
if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
|
|
386
474
|
if details_str is not None and details_str.startswith(
|
|
387
475
|
"Maximum subscriptions reached"
|
|
388
476
|
):
|
|
389
477
|
return MaximumSubscriptionsReachedError(details_str)
|
|
390
|
-
# no cover: start
|
|
478
|
+
# no cover <26.2: start
|
|
479
|
+
# Projections.Delete does this in >=26.2 when projection not stopped.
|
|
391
480
|
return FailedPreconditionError(details_str)
|
|
392
|
-
# no cover: stop
|
|
393
|
-
|
|
481
|
+
# no cover <26.2: stop
|
|
482
|
+
|
|
483
|
+
if e.code() == grpc.StatusCode.PERMISSION_DENIED:
|
|
484
|
+
return AccessDeniedError(details_str)
|
|
485
|
+
|
|
486
|
+
if e.code() == grpc.StatusCode.INTERNAL:
|
|
394
487
|
return InternalError(details_str)
|
|
395
|
-
|
|
488
|
+
|
|
489
|
+
return GrpcError(str(e)) # pragma: no cover
|
|
396
490
|
|
|
397
491
|
|
|
398
492
|
def _convert_append_consistency_violations(
|
|
@@ -572,7 +666,7 @@ class RecordedEventIterator(
|
|
|
572
666
|
self.stop()
|
|
573
667
|
|
|
574
668
|
@abstractmethod
|
|
575
|
-
def stop(self) -> None:
|
|
669
|
+
def stop(self, *, cancel: bool = False) -> None:
|
|
576
670
|
pass # pragma: no cover
|
|
577
671
|
|
|
578
672
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from kurrentdbclient.common import AsyncGrpcStreamers, GrpcStreamers
|
|
6
7
|
from kurrentdbclient.gossip import AsyncGossipService, GossipService
|
|
@@ -21,6 +22,7 @@ if TYPE_CHECKING:
|
|
|
21
22
|
class BaseKurrentDBConnection:
|
|
22
23
|
def __init__(self, grpc_target: str):
|
|
23
24
|
self._grpc_target = grpc_target
|
|
25
|
+
self.connection_id = uuid4()
|
|
24
26
|
|
|
25
27
|
@property
|
|
26
28
|
def grpc_target(self) -> str:
|
|
@@ -75,9 +77,7 @@ class KurrentDBConnection(BaseKurrentDBConnection):
|
|
|
75
77
|
self._grpc_streamers.close()
|
|
76
78
|
# self.grpc_channel.unsubscribe(self._receive_channel_connectivity_state)
|
|
77
79
|
# sleep(0.1) # Allow connectivity polling to stop.
|
|
78
|
-
# print("closing channel")
|
|
79
80
|
self._grpc_channel.close()
|
|
80
|
-
# print("closed channel")
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
class AsyncKurrentDBConnection(BaseKurrentDBConnection):
|
|
@@ -61,9 +61,15 @@ class GrpcDeadlineExceededError(GrpcError, DeadlineExceededError):
|
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class
|
|
64
|
+
class CancelledError(GrpcError):
|
|
65
65
|
"""
|
|
66
|
-
Raised when gRPC operation is cancelled.
|
|
66
|
+
Raised when gRPC operation is somehow cancelled.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CancelledByClientError(CancelledError):
|
|
71
|
+
"""
|
|
72
|
+
Raised when gRPC operation is known to have been cancelled by client.
|
|
67
73
|
"""
|
|
68
74
|
|
|
69
75
|
|
|
@@ -279,6 +279,7 @@ def span_read_stream(
|
|
|
279
279
|
stream_name: str,
|
|
280
280
|
*args: Any,
|
|
281
281
|
**kwargs: Any,
|
|
282
|
+
# TODO: Can't this use AbstractAsyncReadResponse rather than AsyncReadResponse?
|
|
282
283
|
) -> OverloadedSpannerResponse[AbstractReadResponse, AsyncReadResponse]:
|
|
283
284
|
span_name, span_kind = _get_span_name_and_kind(spanned_func)
|
|
284
285
|
with _start_span(tracer, span_name, span_kind) as span:
|
|
@@ -294,6 +295,7 @@ def span_read_stream(
|
|
|
294
295
|
if inspect.iscoroutine(response):
|
|
295
296
|
|
|
296
297
|
async def wrap_response() -> AsyncReadResponse:
|
|
298
|
+
# TODO: Can this be AbstractAsyncReadResponse?
|
|
297
299
|
return cast(
|
|
298
300
|
AsyncReadResponse,
|
|
299
301
|
TracedAsyncReadResponse(
|
|
@@ -317,7 +319,8 @@ def span_read_stream(
|
|
|
317
319
|
span_name=span_name,
|
|
318
320
|
span_kind=span_kind,
|
|
319
321
|
)
|
|
320
|
-
except Exception as e:
|
|
322
|
+
except Exception as e: # pragma: no cover
|
|
323
|
+
# Hard to cover this consistently due to timing of channel readiness.
|
|
321
324
|
_set_span_error(span, e)
|
|
322
325
|
raise
|
|
323
326
|
else:
|
|
@@ -777,15 +780,8 @@ class TracedRecordedEventIterator(
|
|
|
777
780
|
span.end()
|
|
778
781
|
self._current_span = None
|
|
779
782
|
|
|
780
|
-
def stop(self) -> None:
|
|
781
|
-
self.response.stop()
|
|
782
|
-
|
|
783
|
-
def __enter__(self) -> Self:
|
|
784
|
-
self.response.__enter__()
|
|
785
|
-
return self
|
|
786
|
-
|
|
787
|
-
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
788
|
-
return self.response.__exit__(*args, **kwargs)
|
|
783
|
+
def stop(self, *, cancel: bool = False) -> None:
|
|
784
|
+
self.response.stop(cancel=cancel)
|
|
789
785
|
|
|
790
786
|
def __del__(self) -> None:
|
|
791
787
|
current_span = self._current_span
|
|
@@ -597,20 +597,28 @@ class AsyncPersistentSubscription(
|
|
|
597
597
|
return recorded_event
|
|
598
598
|
else: # pragma: no cover
|
|
599
599
|
pass
|
|
600
|
-
except BaseException:
|
|
601
|
-
|
|
600
|
+
except BaseException as e:
|
|
601
|
+
was_cancelled = self._is_cancelled
|
|
602
|
+
await self.stop(timeout=1, cancel=True)
|
|
603
|
+
if was_cancelled and isinstance(e, StopAsyncIteration):
|
|
604
|
+
raise CancelledByClientError from e
|
|
602
605
|
raise
|
|
603
606
|
|
|
604
|
-
async def stop(
|
|
607
|
+
async def stop(
|
|
608
|
+
self,
|
|
609
|
+
*,
|
|
610
|
+
timeout: float | None = None,
|
|
611
|
+
cancel: bool = False,
|
|
612
|
+
) -> None:
|
|
605
613
|
if self._is_context_manager_active:
|
|
606
614
|
self._is_stopping = True
|
|
607
|
-
elif not await self._set_is_stopped():
|
|
615
|
+
elif not await self._set_is_stopped(cancel=cancel):
|
|
608
616
|
try:
|
|
609
617
|
await self._read_reqs.stop(timeout=timeout)
|
|
610
618
|
finally:
|
|
611
619
|
self._read_resp_stream_worker_task.cancel()
|
|
612
620
|
self._stream_stream_call.cancel()
|
|
613
|
-
self.
|
|
621
|
+
self._remove()
|
|
614
622
|
try:
|
|
615
623
|
await self._read_resp_stream_worker_task
|
|
616
624
|
except asyncio.CancelledError:
|
|
@@ -660,6 +668,7 @@ class PersistentSubscription(
|
|
|
660
668
|
self._read_reqs = read_reqs
|
|
661
669
|
self._read_resps = read_resps
|
|
662
670
|
self._is_stopping = False
|
|
671
|
+
self._is_cancelling = False
|
|
663
672
|
|
|
664
673
|
try:
|
|
665
674
|
first_read_resp = self._get_next_read_resp()
|
|
@@ -698,8 +707,14 @@ class PersistentSubscription(
|
|
|
698
707
|
while True:
|
|
699
708
|
try:
|
|
700
709
|
read_resp = self._get_next_read_resp()
|
|
701
|
-
except CancelledByClientError:
|
|
702
|
-
|
|
710
|
+
except CancelledByClientError as e:
|
|
711
|
+
# Grpc stream is cancelled.
|
|
712
|
+
self.stop(cancel=True)
|
|
713
|
+
if self._is_cancelled:
|
|
714
|
+
# Response is stopped and cancelled.
|
|
715
|
+
raise
|
|
716
|
+
# Response is stopped but not cancelled.
|
|
717
|
+
raise StopIteration from e
|
|
703
718
|
content_oneof = read_resp.WhichOneof("content")
|
|
704
719
|
if content_oneof == "event":
|
|
705
720
|
recorded_event = construct_recorded_event(read_resp.event)
|
|
@@ -733,13 +748,14 @@ class PersistentSubscription(
|
|
|
733
748
|
self._grpc_streamers.remove(self)
|
|
734
749
|
self._is_stopped = True
|
|
735
750
|
|
|
736
|
-
def stop(self) -> None:
|
|
751
|
+
def stop(self, *, cancel: bool = False) -> None:
|
|
737
752
|
if self._is_context_manager_active:
|
|
738
753
|
self._is_stopping = True
|
|
739
|
-
|
|
754
|
+
self._is_cancelling = cancel
|
|
755
|
+
elif not self._set_is_stopped(cancel=cancel):
|
|
740
756
|
self._read_reqs.stop()
|
|
741
757
|
self._read_resps.cancel()
|
|
742
|
-
self.
|
|
758
|
+
self._remove()
|
|
743
759
|
|
|
744
760
|
def ack(self, item: UUID | RecordedEvent) -> None:
|
|
745
761
|
self._read_reqs.ack(event_id=self._get_event_id(item))
|
|
@@ -1505,6 +1521,7 @@ class AsyncPersistentSubscriptionsService(
|
|
|
1505
1521
|
expected_group_name=group_name,
|
|
1506
1522
|
stream_name=stream_name,
|
|
1507
1523
|
)
|
|
1524
|
+
await subscription.add_to_steamers()
|
|
1508
1525
|
await subscription.init()
|
|
1509
1526
|
return subscription
|
|
1510
1527
|
|
|
@@ -214,17 +214,25 @@ class AsyncReadResponse(BaseReadResponse, AsyncGrpcStreamer, AbstractAsyncReadRe
|
|
|
214
214
|
)
|
|
215
215
|
if recorded_event is not None:
|
|
216
216
|
return recorded_event
|
|
217
|
-
except BaseException:
|
|
218
|
-
|
|
217
|
+
except BaseException as e:
|
|
218
|
+
was_cancelled = self._is_cancelled
|
|
219
|
+
await self.stop(cancel=True)
|
|
220
|
+
if was_cancelled and isinstance(e, StopAsyncIteration):
|
|
221
|
+
raise CancelledByClientError from e
|
|
219
222
|
raise
|
|
220
223
|
|
|
221
|
-
async def stop(
|
|
224
|
+
async def stop(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
timeout: float | None = None,
|
|
228
|
+
cancel: bool = False,
|
|
229
|
+
) -> None:
|
|
222
230
|
if self._is_context_manager_active:
|
|
223
231
|
self._is_stopping = True
|
|
224
|
-
elif not await self._set_is_stopped():
|
|
232
|
+
elif not await self._set_is_stopped(cancel=cancel):
|
|
225
233
|
self._read_resp_stream_worker_task.cancel()
|
|
226
234
|
self._unary_stream_call.cancel()
|
|
227
|
-
self.
|
|
235
|
+
self._remove()
|
|
228
236
|
try:
|
|
229
237
|
await self._read_resp_stream_worker_task
|
|
230
238
|
except asyncio.CancelledError:
|
|
@@ -284,10 +292,10 @@ class ReadResponse(GrpcStreamer, BaseReadResponse, AbstractReadResponse):
|
|
|
284
292
|
stream_name: str | None,
|
|
285
293
|
grpc_streamers: GrpcStreamers,
|
|
286
294
|
):
|
|
287
|
-
GrpcStreamer.__init__(self, grpc_streamers=grpc_streamers)
|
|
288
295
|
BaseReadResponse.__init__(self, stream_name=stream_name)
|
|
289
296
|
AbstractReadResponse.__init__(self)
|
|
290
297
|
self._read_resps = read_resps
|
|
298
|
+
GrpcStreamer.__init__(self, grpc_streamers=grpc_streamers)
|
|
291
299
|
|
|
292
300
|
def __next__(self) -> RecordedEvent:
|
|
293
301
|
try:
|
|
@@ -298,12 +306,32 @@ class ReadResponse(GrpcStreamer, BaseReadResponse, AbstractReadResponse):
|
|
|
298
306
|
)
|
|
299
307
|
if recorded_event is not None:
|
|
300
308
|
return recorded_event
|
|
301
|
-
except
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
except StopIteration as e:
|
|
310
|
+
# Grpc stream is finished.
|
|
311
|
+
self.stop(cancel=False)
|
|
312
|
+
if self._is_cancelled:
|
|
313
|
+
# Grpc stream is finished but response is cancelled.
|
|
314
|
+
raise CancelledByClientError from e
|
|
315
|
+
# Grpc stream is finished but response is not cancelled.
|
|
306
316
|
raise
|
|
317
|
+
except CancelledByClientError as e:
|
|
318
|
+
# Grpc stream is cancelled.
|
|
319
|
+
self.stop(cancel=True)
|
|
320
|
+
if self._is_cancelled:
|
|
321
|
+
# Response is stopped and cancelled.
|
|
322
|
+
raise
|
|
323
|
+
# Response is stopped but not cancelled.
|
|
324
|
+
raise StopIteration from e
|
|
325
|
+
except Exception as e:
|
|
326
|
+
# Some channel error.
|
|
327
|
+
if self._is_cancelled:
|
|
328
|
+
# Some channel error but response was cancelled.
|
|
329
|
+
# print("Was cancelled, got error:", e)
|
|
330
|
+
raise CancelledByClientError from e
|
|
331
|
+
# Some channel error but response was not cancelled.
|
|
332
|
+
# print(f"Not cancelled ({id(self)}), got error:", e)
|
|
333
|
+
self.stop(cancel=True)
|
|
334
|
+
raise e
|
|
307
335
|
|
|
308
336
|
def _get_next_read_resp(self) -> streams_pb2.ReadResp:
|
|
309
337
|
try:
|
|
@@ -314,10 +342,10 @@ class ReadResponse(GrpcStreamer, BaseReadResponse, AbstractReadResponse):
|
|
|
314
342
|
assert isinstance(read_resp, streams_pb2.ReadResp)
|
|
315
343
|
return read_resp
|
|
316
344
|
|
|
317
|
-
def stop(self) -> None:
|
|
318
|
-
if not self._set_is_stopped():
|
|
345
|
+
def stop(self, *, cancel: bool = False) -> None:
|
|
346
|
+
if not self._set_is_stopped(cancel=cancel):
|
|
319
347
|
self._read_resps.cancel()
|
|
320
|
-
self.
|
|
348
|
+
self._remove()
|
|
321
349
|
|
|
322
350
|
|
|
323
351
|
class CatchupSubscription(ReadResponse, AbstractCatchupSubscription):
|
|
@@ -988,6 +1016,7 @@ class AsyncStreamsService(BaseStreamsService[AsyncGrpcStreamers]):
|
|
|
988
1016
|
stream_name=stream_name,
|
|
989
1017
|
grpc_streamers=self._grpc_streamers,
|
|
990
1018
|
)
|
|
1019
|
+
await response.add_to_steamers()
|
|
991
1020
|
else:
|
|
992
1021
|
response = AsyncCatchupSubscription(
|
|
993
1022
|
unary_stream_call=unary_stream_call,
|
|
@@ -997,6 +1026,7 @@ class AsyncStreamsService(BaseStreamsService[AsyncGrpcStreamers]):
|
|
|
997
1026
|
include_fell_behind=include_fell_behind,
|
|
998
1027
|
grpc_streamers=self._grpc_streamers,
|
|
999
1028
|
)
|
|
1029
|
+
await response.add_to_steamers()
|
|
1000
1030
|
try:
|
|
1001
1031
|
await response.check_confirmation()
|
|
1002
1032
|
except BaseException:
|
|
@@ -1184,6 +1214,8 @@ class StreamsService(BaseStreamsService[GrpcStreamers]):
|
|
|
1184
1214
|
)
|
|
1185
1215
|
|
|
1186
1216
|
# Send the read request, and iterate over the response.
|
|
1217
|
+
# - if channel is closed, this raises ValueError('Channel closed!').
|
|
1218
|
+
# - although a fresh channel doesn't do this,
|
|
1187
1219
|
read_resps = self._stub.Read(
|
|
1188
1220
|
read_req,
|
|
1189
1221
|
timeout=timeout,
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kurrentdbclient"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.6"
|
|
8
8
|
description = "Python gRPC Client for KurrentDB"
|
|
9
9
|
license = { text = "BSD-3-Clause" }
|
|
10
10
|
readme = "README.md"
|
|
@@ -204,7 +204,7 @@ branch = true
|
|
|
204
204
|
omit = [
|
|
205
205
|
"kurrentdbclient/protos/*"
|
|
206
206
|
]
|
|
207
|
-
relative_files = true
|
|
207
|
+
#relative_files = true
|
|
208
208
|
|
|
209
209
|
[tool.coverage.report]
|
|
210
210
|
fail_under = 100
|
|
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
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2.pyi
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/google/rpc/code_pb2_grpc.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.pyi
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/cluster_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/gossip_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/persistent_pb2.pyi
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/persistent_pb2_grpc.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2.pyi
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/projections_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/shared_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/status_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v1/streams_pb2_grpc.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2.pyi
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/errors_pb2_grpc.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/streams_pb2.py
RENAMED
|
File without changes
|
{kurrentdbclient-1.3.4 → kurrentdbclient-1.3.6}/kurrentdbclient/protos/v2/streams/streams_pb2.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|