kurrentdbclient 0.3__py3-none-any.whl
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/__init__.py +49 -0
- kurrentdbclient/asyncio_client.py +1662 -0
- kurrentdbclient/client.py +1914 -0
- kurrentdbclient/common.py +535 -0
- kurrentdbclient/connection.py +107 -0
- kurrentdbclient/connection_spec.py +371 -0
- kurrentdbclient/events.py +141 -0
- kurrentdbclient/exceptions.py +239 -0
- kurrentdbclient/gossip.py +104 -0
- kurrentdbclient/instrumentation/__init__.py +0 -0
- kurrentdbclient/instrumentation/opentelemetry/__init__.py +185 -0
- kurrentdbclient/instrumentation/opentelemetry/attributes.py +20 -0
- kurrentdbclient/instrumentation/opentelemetry/grpc.py +165 -0
- kurrentdbclient/instrumentation/opentelemetry/package.py +2 -0
- kurrentdbclient/instrumentation/opentelemetry/spanners.py +1097 -0
- kurrentdbclient/instrumentation/opentelemetry/utils.py +199 -0
- kurrentdbclient/instrumentation/opentelemetry/version.py +2 -0
- kurrentdbclient/persistent.py +1982 -0
- kurrentdbclient/projections.py +735 -0
- kurrentdbclient/protos/Grpc/cluster_pb2.py +92 -0
- kurrentdbclient/protos/Grpc/cluster_pb2.pyi +765 -0
- kurrentdbclient/protos/Grpc/cluster_pb2_grpc.py +514 -0
- kurrentdbclient/protos/Grpc/code_pb2.py +37 -0
- kurrentdbclient/protos/Grpc/code_pb2.pyi +357 -0
- kurrentdbclient/protos/Grpc/code_pb2_grpc.py +24 -0
- kurrentdbclient/protos/Grpc/gossip_pb2.py +46 -0
- kurrentdbclient/protos/Grpc/gossip_pb2.pyi +126 -0
- kurrentdbclient/protos/Grpc/gossip_pb2_grpc.py +98 -0
- kurrentdbclient/protos/Grpc/persistent_pb2.py +140 -0
- kurrentdbclient/protos/Grpc/persistent_pb2.pyi +1135 -0
- kurrentdbclient/protos/Grpc/persistent_pb2_grpc.py +399 -0
- kurrentdbclient/protos/Grpc/projections_pb2.py +99 -0
- kurrentdbclient/protos/Grpc/projections_pb2.pyi +558 -0
- kurrentdbclient/protos/Grpc/projections_pb2_grpc.py +485 -0
- kurrentdbclient/protos/Grpc/shared_pb2.py +62 -0
- kurrentdbclient/protos/Grpc/shared_pb2.pyi +218 -0
- kurrentdbclient/protos/Grpc/shared_pb2_grpc.py +24 -0
- kurrentdbclient/protos/Grpc/status_pb2.py +39 -0
- kurrentdbclient/protos/Grpc/status_pb2.pyi +67 -0
- kurrentdbclient/protos/Grpc/status_pb2_grpc.py +24 -0
- kurrentdbclient/protos/Grpc/streams_pb2.py +132 -0
- kurrentdbclient/protos/Grpc/streams_pb2.pyi +1038 -0
- kurrentdbclient/protos/Grpc/streams_pb2_grpc.py +269 -0
- kurrentdbclient/py.typed +0 -0
- kurrentdbclient/streams.py +1400 -0
- kurrentdbclient-0.3.dist-info/LICENSE +29 -0
- kurrentdbclient-0.3.dist-info/METADATA +3769 -0
- kurrentdbclient-0.3.dist-info/RECORD +49 -0
- kurrentdbclient-0.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import datetime
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from base64 import b64encode
|
|
10
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
AsyncIterator,
|
|
15
|
+
Generic,
|
|
16
|
+
Iterator,
|
|
17
|
+
Literal,
|
|
18
|
+
Optional,
|
|
19
|
+
Sequence,
|
|
20
|
+
Tuple,
|
|
21
|
+
TypeVar,
|
|
22
|
+
Union,
|
|
23
|
+
)
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
from weakref import WeakValueDictionary
|
|
26
|
+
|
|
27
|
+
import grpc
|
|
28
|
+
import grpc.aio
|
|
29
|
+
from typing_extensions import Self
|
|
30
|
+
|
|
31
|
+
from kurrentdbclient.connection_spec import ConnectionSpec
|
|
32
|
+
from kurrentdbclient.events import RecordedEvent
|
|
33
|
+
from kurrentdbclient.exceptions import (
|
|
34
|
+
AbortedByServer,
|
|
35
|
+
AlreadyExists,
|
|
36
|
+
CancelledByClient,
|
|
37
|
+
ConsumerTooSlow,
|
|
38
|
+
ExceptionThrownByHandler,
|
|
39
|
+
FailedPrecondition,
|
|
40
|
+
GrpcDeadlineExceeded,
|
|
41
|
+
GrpcError,
|
|
42
|
+
InternalError,
|
|
43
|
+
KurrentDBClientException,
|
|
44
|
+
MaximumSubscriptionsReached,
|
|
45
|
+
NodeIsNotLeader,
|
|
46
|
+
NotFound,
|
|
47
|
+
OperationFailed,
|
|
48
|
+
ServiceUnavailable,
|
|
49
|
+
SSLError,
|
|
50
|
+
UnknownError,
|
|
51
|
+
)
|
|
52
|
+
from kurrentdbclient.protos.Grpc import persistent_pb2, streams_pb2
|
|
53
|
+
|
|
54
|
+
# Avoid ares resolver.
|
|
55
|
+
if "GRPC_DNS_RESOLVER" not in os.environ:
|
|
56
|
+
os.environ["GRPC_DNS_RESOLVER"] = "native"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
60
|
+
from grpc import Metadata
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
Metadata = Tuple[Tuple[str, str], ...]
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"handle_rpc_error",
|
|
67
|
+
"BasicAuthCallCredentials",
|
|
68
|
+
"KurrentDBService",
|
|
69
|
+
"Metadata",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
PROTOBUF_MAX_DEADLINE_SECONDS = 315576000000
|
|
74
|
+
DEFAULT_CHECKPOINT_INTERVAL_MULTIPLIER = 5
|
|
75
|
+
DEFAULT_WINDOW_SIZE = 30
|
|
76
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MESSAGE_TIMEOUT = 30.0
|
|
77
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MAX_RETRY_COUNT = 10
|
|
78
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MIN_CHECKPOINT_COUNT = 10
|
|
79
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MAX_CHECKPOINT_COUNT = 1000
|
|
80
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_CHECKPOINT_AFTER = 2.0
|
|
81
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_EVENT_BUFFER_SIZE = 150
|
|
82
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MAX_ACK_BATCH_SIZE = 50
|
|
83
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MAX_ACK_DELAY = 0.2
|
|
84
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_STOPPING_GRACE = 0.2
|
|
85
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_MAX_SUBSCRIBER_COUNT = 5
|
|
86
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_LIVE_BUFFER_SIZE = 500
|
|
87
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_READ_BATCH_SIZE = 200
|
|
88
|
+
DEFAULT_PERSISTENT_SUBSCRIPTION_HISTORY_BUFFER_SIZE = 500
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
GrpcOption = Tuple[str, Union[str, int]]
|
|
92
|
+
GrpcOptions = Tuple[GrpcOption, ...]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BaseGrpcStreamer(ABC):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class GrpcStreamer(BaseGrpcStreamer):
|
|
100
|
+
def __init__(self, grpc_streamers: GrpcStreamers) -> None:
|
|
101
|
+
self._grpc_streamers = grpc_streamers
|
|
102
|
+
self._grpc_streamers.add(self)
|
|
103
|
+
self._is_stopped = False
|
|
104
|
+
self._is_stopped_lock = threading.Lock()
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def stop(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Stops the iterator(s) of streaming call.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def _set_is_stopped(self) -> bool:
|
|
113
|
+
is_stopped = True
|
|
114
|
+
if self._is_stopped is False:
|
|
115
|
+
with self._is_stopped_lock:
|
|
116
|
+
if self._is_stopped is False:
|
|
117
|
+
is_stopped = False
|
|
118
|
+
self._is_stopped = True
|
|
119
|
+
else: # pragma: no cover
|
|
120
|
+
pass
|
|
121
|
+
return is_stopped
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AsyncGrpcStreamer(BaseGrpcStreamer):
|
|
125
|
+
def __init__(self, grpc_streamers: AsyncGrpcStreamers) -> None:
|
|
126
|
+
self._grpc_streamers = grpc_streamers
|
|
127
|
+
self._grpc_streamers.add(self)
|
|
128
|
+
self._is_stopped = False
|
|
129
|
+
self._is_stopped_lock = asyncio.Lock()
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
async def stop(self) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Stops the iterator(s) of streaming call.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
async def _set_is_stopped(self) -> bool:
|
|
138
|
+
is_stopped = True
|
|
139
|
+
if self._is_stopped is False:
|
|
140
|
+
async with self._is_stopped_lock:
|
|
141
|
+
if self._is_stopped is False:
|
|
142
|
+
is_stopped = False
|
|
143
|
+
self._is_stopped = True
|
|
144
|
+
else: # pragma: no cover
|
|
145
|
+
pass
|
|
146
|
+
return is_stopped
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
TGrpcStreamer = TypeVar("TGrpcStreamer", bound=BaseGrpcStreamer)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class BaseGrpcStreamers(Generic[TGrpcStreamer]):
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
self.map: WeakValueDictionary[int, TGrpcStreamer] = WeakValueDictionary()
|
|
155
|
+
self.lock = threading.Lock()
|
|
156
|
+
|
|
157
|
+
def add(self, streamer: TGrpcStreamer) -> None:
|
|
158
|
+
with self.lock:
|
|
159
|
+
self.map[id(streamer)] = streamer
|
|
160
|
+
|
|
161
|
+
def __iter__(self) -> Iterator[TGrpcStreamer]:
|
|
162
|
+
with self.lock:
|
|
163
|
+
return iter(tuple(self.map.values()))
|
|
164
|
+
|
|
165
|
+
def remove(self, streamer: TGrpcStreamer) -> None:
|
|
166
|
+
with self.lock:
|
|
167
|
+
try:
|
|
168
|
+
self.map.pop(id(streamer))
|
|
169
|
+
except KeyError: # pragma: no cover
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class GrpcStreamers(BaseGrpcStreamers[GrpcStreamer]):
|
|
174
|
+
def close(self) -> None:
|
|
175
|
+
for grpc_streamer in self:
|
|
176
|
+
# print("closing streamer")
|
|
177
|
+
grpc_streamer.stop()
|
|
178
|
+
# print("closed streamer")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class AsyncGrpcStreamers(BaseGrpcStreamers[AsyncGrpcStreamer]):
|
|
182
|
+
async def close(self) -> None:
|
|
183
|
+
for async_grpc_streamer in self:
|
|
184
|
+
# print("closing streamer")
|
|
185
|
+
await async_grpc_streamer.stop()
|
|
186
|
+
# print("closed streamer")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
TGrpcStreamers = TypeVar("TGrpcStreamers", bound=BaseGrpcStreamers[Any])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class BasicAuthCallCredentials(grpc.AuthMetadataPlugin):
|
|
193
|
+
def __init__(self, username: str, password: str):
|
|
194
|
+
credentials = b64encode(f"{username}:{password}".encode())
|
|
195
|
+
self._metadata = (("authorization", (b"Basic " + credentials)),)
|
|
196
|
+
|
|
197
|
+
def __call__(
|
|
198
|
+
self,
|
|
199
|
+
context: grpc.AuthMetadataContext,
|
|
200
|
+
callback: grpc.AuthMetadataPluginCallback,
|
|
201
|
+
) -> None:
|
|
202
|
+
callback(self._metadata, None)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def handle_rpc_error(e: grpc.RpcError) -> KurrentDBClientException: # noqa: C901
|
|
206
|
+
"""
|
|
207
|
+
Converts gRPC errors to client exceptions.
|
|
208
|
+
"""
|
|
209
|
+
if isinstance(e, (grpc.Call, grpc.aio.AioRpcError)):
|
|
210
|
+
if e.code() == grpc.StatusCode.UNKNOWN:
|
|
211
|
+
details = str(e.details())
|
|
212
|
+
if "Exception was thrown by handler" in details:
|
|
213
|
+
return ExceptionThrownByHandler(e)
|
|
214
|
+
elif (
|
|
215
|
+
"Envelope callback expected Updated, received Conflict instead"
|
|
216
|
+
in details
|
|
217
|
+
):
|
|
218
|
+
# Projections.Create does this....
|
|
219
|
+
return AlreadyExists(e)
|
|
220
|
+
elif (
|
|
221
|
+
"Envelope callback expected Updated, received NotFound instead"
|
|
222
|
+
in details
|
|
223
|
+
):
|
|
224
|
+
# Projections.Update and Projections.Delete does this in < v24.6
|
|
225
|
+
return NotFound(e) # pragma: no cover
|
|
226
|
+
elif (
|
|
227
|
+
"Envelope callback expected Statistics, received NotFound instead"
|
|
228
|
+
in details
|
|
229
|
+
):
|
|
230
|
+
# Projections.Statistics does this in < v24.6
|
|
231
|
+
return NotFound(e) # pragma: no cover
|
|
232
|
+
elif (
|
|
233
|
+
"Envelope callback expected ProjectionState, received NotFound instead"
|
|
234
|
+
in details
|
|
235
|
+
):
|
|
236
|
+
# Projections.State does this in < v24.6
|
|
237
|
+
return NotFound(e) # pragma: no cover
|
|
238
|
+
elif (
|
|
239
|
+
"Envelope callback expected ProjectionResult, received NotFound instead"
|
|
240
|
+
in details
|
|
241
|
+
):
|
|
242
|
+
# Projections.Result does this in < v24.6
|
|
243
|
+
return NotFound(e) # pragma: no cover
|
|
244
|
+
elif (
|
|
245
|
+
"Envelope callback expected Updated, received OperationFailed instead"
|
|
246
|
+
in details
|
|
247
|
+
):
|
|
248
|
+
# Projections.Delete does this....
|
|
249
|
+
return OperationFailed(e)
|
|
250
|
+
else: # pragma: no cover
|
|
251
|
+
return UnknownError(e)
|
|
252
|
+
elif e.code() == grpc.StatusCode.ABORTED:
|
|
253
|
+
details = str(e.details())
|
|
254
|
+
if isinstance(details, str) and "Consumer too slow" in details:
|
|
255
|
+
return ConsumerTooSlow()
|
|
256
|
+
else:
|
|
257
|
+
return AbortedByServer()
|
|
258
|
+
elif (
|
|
259
|
+
e.code() == grpc.StatusCode.CANCELLED
|
|
260
|
+
and e.details() == "Locally cancelled by application!"
|
|
261
|
+
):
|
|
262
|
+
return CancelledByClient(e)
|
|
263
|
+
elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
264
|
+
return GrpcDeadlineExceeded(e)
|
|
265
|
+
elif e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
266
|
+
details = e.details() or ""
|
|
267
|
+
if "SSL_ERROR" in details:
|
|
268
|
+
# root_certificates is None and CA cert not installed
|
|
269
|
+
return SSLError(e)
|
|
270
|
+
if "empty address list:" in details:
|
|
271
|
+
# given root_certificates is invalid
|
|
272
|
+
return SSLError(e)
|
|
273
|
+
return ServiceUnavailable(details)
|
|
274
|
+
elif e.code() == grpc.StatusCode.ALREADY_EXISTS:
|
|
275
|
+
return AlreadyExists(e.details())
|
|
276
|
+
elif e.code() == grpc.StatusCode.NOT_FOUND:
|
|
277
|
+
if e.details() == "Leader info available":
|
|
278
|
+
return NodeIsNotLeader(e)
|
|
279
|
+
return NotFound()
|
|
280
|
+
elif e.code() == grpc.StatusCode.FAILED_PRECONDITION:
|
|
281
|
+
details = str(e.details())
|
|
282
|
+
if details is not None and details.startswith(
|
|
283
|
+
"Maximum subscriptions reached"
|
|
284
|
+
):
|
|
285
|
+
return MaximumSubscriptionsReached(details)
|
|
286
|
+
else: # pragma: no cover
|
|
287
|
+
return FailedPrecondition(details)
|
|
288
|
+
elif e.code() == grpc.StatusCode.INTERNAL: # pragma: no cover
|
|
289
|
+
return InternalError(e.details())
|
|
290
|
+
return GrpcError(e)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class KurrentDBService(Generic[TGrpcStreamers]):
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
connection_spec: ConnectionSpec,
|
|
297
|
+
grpc_streamers: TGrpcStreamers,
|
|
298
|
+
):
|
|
299
|
+
self._connection_spec = connection_spec
|
|
300
|
+
self._grpc_streamers = grpc_streamers
|
|
301
|
+
|
|
302
|
+
def _metadata(
|
|
303
|
+
self, metadata: Optional[Metadata], requires_leader: bool = False
|
|
304
|
+
) -> Metadata:
|
|
305
|
+
default = (
|
|
306
|
+
"true"
|
|
307
|
+
if self._connection_spec.options.NodePreference == "leader"
|
|
308
|
+
else "false"
|
|
309
|
+
)
|
|
310
|
+
requires_leader_metadata: Metadata = (
|
|
311
|
+
("requires-leader", "true" if requires_leader else default),
|
|
312
|
+
)
|
|
313
|
+
metadata = tuple() if metadata is None else metadata
|
|
314
|
+
return metadata + requires_leader_metadata
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def construct_filter_include_regex(patterns: Sequence[str]) -> str:
|
|
318
|
+
patterns = [patterns] if isinstance(patterns, str) else patterns
|
|
319
|
+
return "^" + "|".join(patterns) + "$"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def construct_filter_exclude_regex(patterns: Sequence[str]) -> str:
|
|
323
|
+
patterns = [patterns] if isinstance(patterns, str) else patterns
|
|
324
|
+
return "^(?!(" + "|".join([s + "$" for s in patterns]) + "))"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def construct_recorded_event(
|
|
328
|
+
read_event: Union[
|
|
329
|
+
streams_pb2.ReadResp.ReadEvent, persistent_pb2.ReadResp.ReadEvent
|
|
330
|
+
],
|
|
331
|
+
) -> Optional[RecordedEvent]:
|
|
332
|
+
assert isinstance(
|
|
333
|
+
read_event, (streams_pb2.ReadResp.ReadEvent, persistent_pb2.ReadResp.ReadEvent)
|
|
334
|
+
)
|
|
335
|
+
event = read_event.event
|
|
336
|
+
assert isinstance(
|
|
337
|
+
event,
|
|
338
|
+
(
|
|
339
|
+
streams_pb2.ReadResp.ReadEvent.RecordedEvent,
|
|
340
|
+
persistent_pb2.ReadResp.ReadEvent.RecordedEvent,
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
link = read_event.link
|
|
344
|
+
assert isinstance(
|
|
345
|
+
link,
|
|
346
|
+
(
|
|
347
|
+
streams_pb2.ReadResp.ReadEvent.RecordedEvent,
|
|
348
|
+
persistent_pb2.ReadResp.ReadEvent.RecordedEvent,
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if event.id.string == "": # pragma: no cover
|
|
353
|
+
# Sometimes get here when resolving links after deleting a stream.
|
|
354
|
+
# Sometimes never, e.g. when the test suite runs, don't know why.
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
position_oneof = read_event.WhichOneof("position")
|
|
358
|
+
if position_oneof == "commit_position":
|
|
359
|
+
ignore_commit_position = False
|
|
360
|
+
# # Is this equality always true? Ans: Not when resolve_links=True.
|
|
361
|
+
# assert read_event.commit_position == event.commit_position
|
|
362
|
+
else: # pragma: no cover
|
|
363
|
+
# We get here with KurrentDB < 22.10 when reading a stream.
|
|
364
|
+
assert position_oneof == "no_position", position_oneof
|
|
365
|
+
ignore_commit_position = True
|
|
366
|
+
|
|
367
|
+
if isinstance(read_event, persistent_pb2.ReadResp.ReadEvent):
|
|
368
|
+
retry_count: Optional[int] = read_event.retry_count
|
|
369
|
+
else:
|
|
370
|
+
retry_count = None
|
|
371
|
+
|
|
372
|
+
if link.id.string == "":
|
|
373
|
+
recorded_event_link: Optional[RecordedEvent] = None
|
|
374
|
+
else:
|
|
375
|
+
try:
|
|
376
|
+
recorded_at = datetime.datetime.fromtimestamp(
|
|
377
|
+
int(event.metadata.get("created", "")) / 10000000.0,
|
|
378
|
+
tz=datetime.timezone.utc,
|
|
379
|
+
)
|
|
380
|
+
except (TypeError, ValueError): # pragma: no cover
|
|
381
|
+
recorded_at = None
|
|
382
|
+
|
|
383
|
+
recorded_event_link = RecordedEvent(
|
|
384
|
+
id=UUID(link.id.string),
|
|
385
|
+
type=link.metadata.get("type", ""),
|
|
386
|
+
data=link.data,
|
|
387
|
+
metadata=link.custom_metadata,
|
|
388
|
+
content_type=link.metadata.get("content-type", ""),
|
|
389
|
+
stream_name=link.stream_identifier.stream_name.decode("utf8"),
|
|
390
|
+
stream_position=link.stream_revision,
|
|
391
|
+
commit_position=None if ignore_commit_position else link.commit_position,
|
|
392
|
+
prepare_position=None if ignore_commit_position else link.prepare_position,
|
|
393
|
+
retry_count=retry_count,
|
|
394
|
+
recorded_at=recorded_at,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
recorded_at = datetime.datetime.fromtimestamp(
|
|
399
|
+
int(event.metadata.get("created", "")) / 10000000.0,
|
|
400
|
+
tz=datetime.timezone.utc,
|
|
401
|
+
)
|
|
402
|
+
except (TypeError, ValueError): # pragma: no cover
|
|
403
|
+
recorded_at = None
|
|
404
|
+
|
|
405
|
+
recorded_event = RecordedEvent(
|
|
406
|
+
id=UUID(event.id.string),
|
|
407
|
+
type=event.metadata.get("type", ""),
|
|
408
|
+
data=event.data,
|
|
409
|
+
metadata=event.custom_metadata,
|
|
410
|
+
content_type=event.metadata.get("content-type", ""),
|
|
411
|
+
stream_name=event.stream_identifier.stream_name.decode("utf8"),
|
|
412
|
+
stream_position=event.stream_revision,
|
|
413
|
+
commit_position=None if ignore_commit_position else event.commit_position,
|
|
414
|
+
prepare_position=None if ignore_commit_position else event.prepare_position,
|
|
415
|
+
retry_count=retry_count,
|
|
416
|
+
link=recorded_event_link,
|
|
417
|
+
recorded_at=recorded_at,
|
|
418
|
+
)
|
|
419
|
+
return recorded_event
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
try: # pragma: no cover
|
|
423
|
+
_ContextManager = AbstractContextManager[Iterator[RecordedEvent]]
|
|
424
|
+
except TypeError: # pragma: no cover
|
|
425
|
+
# For Python <= v3.9.
|
|
426
|
+
_ContextManager = AbstractContextManager # type: ignore
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class RecordedEventIterator(Iterator[RecordedEvent], _ContextManager):
|
|
430
|
+
def __iter__(self) -> Self:
|
|
431
|
+
return self
|
|
432
|
+
|
|
433
|
+
def __enter__(self) -> Self:
|
|
434
|
+
return self
|
|
435
|
+
|
|
436
|
+
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
|
437
|
+
self.stop()
|
|
438
|
+
|
|
439
|
+
def __del__(self) -> None:
|
|
440
|
+
self.stop()
|
|
441
|
+
|
|
442
|
+
@abstractmethod
|
|
443
|
+
def stop(self) -> None:
|
|
444
|
+
pass # pragma: no cover
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class AbstractReadResponse(RecordedEventIterator):
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class RecordedEventSubscription(RecordedEventIterator):
|
|
452
|
+
@property
|
|
453
|
+
@abstractmethod
|
|
454
|
+
def subscription_id(self) -> str:
|
|
455
|
+
pass # pragma: no cover
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class AbstractCatchupSubscription(RecordedEventSubscription, AbstractReadResponse):
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class AbstractPersistentSubscription(RecordedEventSubscription):
|
|
463
|
+
@abstractmethod
|
|
464
|
+
def ack(self, item: Union[UUID, RecordedEvent]) -> None:
|
|
465
|
+
pass # pragma: no cover
|
|
466
|
+
|
|
467
|
+
@abstractmethod
|
|
468
|
+
def nack(
|
|
469
|
+
self,
|
|
470
|
+
item: Union[UUID, RecordedEvent],
|
|
471
|
+
action: Literal["unknown", "park", "retry", "skip", "stop"],
|
|
472
|
+
) -> None:
|
|
473
|
+
pass # pragma: no cover
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
try: # pragma: no cover
|
|
477
|
+
_AsyncContextManager = AbstractAsyncContextManager[AsyncIterator[RecordedEvent]]
|
|
478
|
+
except TypeError: # pragma: no cover
|
|
479
|
+
# For Python <= v3.9.
|
|
480
|
+
_AsyncContextManager = AbstractAsyncContextManager # type: ignore
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class AsyncRecordedEventIterator(AsyncIterator[RecordedEvent], _AsyncContextManager):
|
|
484
|
+
@abstractmethod
|
|
485
|
+
async def stop(self) -> None:
|
|
486
|
+
pass # pragma: no cover
|
|
487
|
+
|
|
488
|
+
def __aiter__(self) -> Self:
|
|
489
|
+
return self
|
|
490
|
+
|
|
491
|
+
async def __aenter__(self) -> Self:
|
|
492
|
+
return self
|
|
493
|
+
|
|
494
|
+
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
495
|
+
await self.stop()
|
|
496
|
+
|
|
497
|
+
def _set_iter_error_for_testing(self) -> None:
|
|
498
|
+
# This, because I can't find a good way to inspire an error during iterating
|
|
499
|
+
# with catchup and persistent subscriptions after successfully
|
|
500
|
+
# receiving confirmation response, tried closing the channel
|
|
501
|
+
# but the async streaming response continues (unlike with sync call). Needed
|
|
502
|
+
# to inspire this error to test instrumentation span error during iteration.
|
|
503
|
+
self._iter_error_for_testing = True
|
|
504
|
+
|
|
505
|
+
def _has_iter_error_for_testing(self) -> bool:
|
|
506
|
+
return getattr(self, "_iter_error_for_testing", False)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class AbstractAsyncReadResponse(AsyncRecordedEventIterator):
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class AsyncRecordedEventSubscription(AsyncRecordedEventIterator):
|
|
514
|
+
@property
|
|
515
|
+
@abstractmethod
|
|
516
|
+
def subscription_id(self) -> str:
|
|
517
|
+
pass # pragma: no cover
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class AbstractAsyncCatchupSubscription(AsyncRecordedEventSubscription):
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class AbstractAsyncPersistentSubscription(AsyncRecordedEventSubscription):
|
|
525
|
+
@abstractmethod
|
|
526
|
+
async def ack(self, item: Union[UUID, RecordedEvent]) -> None:
|
|
527
|
+
pass # pragma: no cover
|
|
528
|
+
|
|
529
|
+
@abstractmethod
|
|
530
|
+
async def nack(
|
|
531
|
+
self,
|
|
532
|
+
item: Union[UUID, RecordedEvent],
|
|
533
|
+
action: Literal["unknown", "park", "retry", "skip", "stop"],
|
|
534
|
+
) -> None:
|
|
535
|
+
pass # pragma: no cover
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import grpc.aio
|
|
4
|
+
|
|
5
|
+
from kurrentdbclient.common import AsyncGrpcStreamers, GrpcStreamers
|
|
6
|
+
from kurrentdbclient.connection_spec import ConnectionSpec
|
|
7
|
+
from kurrentdbclient.gossip import AsyncGossipService, GossipService
|
|
8
|
+
from kurrentdbclient.persistent import (
|
|
9
|
+
AsyncPersistentSubscriptionsService,
|
|
10
|
+
PersistentSubscriptionsService,
|
|
11
|
+
)
|
|
12
|
+
from kurrentdbclient.projections import AsyncProjectionsService, ProjectionsService
|
|
13
|
+
from kurrentdbclient.streams import AsyncStreamsService, StreamsService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseKurrentDBConnection:
|
|
17
|
+
def __init__(self, grpc_target: str):
|
|
18
|
+
self._grpc_target = grpc_target
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def grpc_target(self) -> str:
|
|
22
|
+
return self._grpc_target
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KurrentDBConnection(BaseKurrentDBConnection):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
grpc_channel: grpc.Channel,
|
|
29
|
+
grpc_target: str,
|
|
30
|
+
connection_spec: ConnectionSpec,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(grpc_target)
|
|
33
|
+
self._grpc_channel = grpc_channel
|
|
34
|
+
self._grpc_streamers = GrpcStreamers()
|
|
35
|
+
self.gossip = GossipService(
|
|
36
|
+
channel=grpc_channel,
|
|
37
|
+
connection_spec=connection_spec,
|
|
38
|
+
grpc_streamers=self._grpc_streamers,
|
|
39
|
+
)
|
|
40
|
+
self.streams = StreamsService(
|
|
41
|
+
grpc_channel=grpc_channel,
|
|
42
|
+
connection_spec=connection_spec,
|
|
43
|
+
grpc_streamers=self._grpc_streamers,
|
|
44
|
+
)
|
|
45
|
+
self.persistent_subscriptions = PersistentSubscriptionsService(
|
|
46
|
+
channel=grpc_channel,
|
|
47
|
+
connection_spec=connection_spec,
|
|
48
|
+
grpc_streamers=self._grpc_streamers,
|
|
49
|
+
)
|
|
50
|
+
self.projections = ProjectionsService(
|
|
51
|
+
channel=grpc_channel,
|
|
52
|
+
connection_spec=connection_spec,
|
|
53
|
+
grpc_streamers=self._grpc_streamers,
|
|
54
|
+
)
|
|
55
|
+
# self._channel_connectivity_state: Optional[ChannelConnectivity] = None
|
|
56
|
+
# self.grpc_channel.subscribe(self._receive_channel_connectivity_state)
|
|
57
|
+
|
|
58
|
+
# def _receive_channel_connectivity_state(
|
|
59
|
+
# self, connectivity: ChannelConnectivity
|
|
60
|
+
# ) -> None:
|
|
61
|
+
# self._channel_connectivity_state = connectivity
|
|
62
|
+
# # print("Channel connectivity state:", connectivity)
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
self._grpc_streamers.close()
|
|
66
|
+
# self.grpc_channel.unsubscribe(self._receive_channel_connectivity_state)
|
|
67
|
+
# sleep(0.1) # Allow connectivity polling to stop.
|
|
68
|
+
# print("closing channel")
|
|
69
|
+
self._grpc_channel.close()
|
|
70
|
+
# print("closed channel")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AsyncKurrentDBConnection(BaseKurrentDBConnection):
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
grpc_channel: grpc.aio.Channel,
|
|
77
|
+
grpc_target: str,
|
|
78
|
+
connection_spec: ConnectionSpec,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__(grpc_target)
|
|
81
|
+
self._grpc_channel = grpc_channel
|
|
82
|
+
self._grpc_target = grpc_target
|
|
83
|
+
self._grpc_streamers = AsyncGrpcStreamers()
|
|
84
|
+
self.gossip = AsyncGossipService(
|
|
85
|
+
grpc_channel,
|
|
86
|
+
connection_spec=connection_spec,
|
|
87
|
+
grpc_streamers=self._grpc_streamers,
|
|
88
|
+
)
|
|
89
|
+
self.streams = AsyncStreamsService(
|
|
90
|
+
grpc_channel,
|
|
91
|
+
connection_spec=connection_spec,
|
|
92
|
+
grpc_streamers=self._grpc_streamers,
|
|
93
|
+
)
|
|
94
|
+
self.persistent_subscriptions = AsyncPersistentSubscriptionsService(
|
|
95
|
+
grpc_channel,
|
|
96
|
+
connection_spec=connection_spec,
|
|
97
|
+
grpc_streamers=self._grpc_streamers,
|
|
98
|
+
)
|
|
99
|
+
self.projections = AsyncProjectionsService(
|
|
100
|
+
grpc_channel,
|
|
101
|
+
connection_spec=connection_spec,
|
|
102
|
+
grpc_streamers=self._grpc_streamers,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def close(self) -> None:
|
|
106
|
+
await self._grpc_streamers.close()
|
|
107
|
+
await self._grpc_channel.close(grace=5)
|