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.
Files changed (49) hide show
  1. kurrentdbclient/__init__.py +49 -0
  2. kurrentdbclient/asyncio_client.py +1662 -0
  3. kurrentdbclient/client.py +1914 -0
  4. kurrentdbclient/common.py +535 -0
  5. kurrentdbclient/connection.py +107 -0
  6. kurrentdbclient/connection_spec.py +371 -0
  7. kurrentdbclient/events.py +141 -0
  8. kurrentdbclient/exceptions.py +239 -0
  9. kurrentdbclient/gossip.py +104 -0
  10. kurrentdbclient/instrumentation/__init__.py +0 -0
  11. kurrentdbclient/instrumentation/opentelemetry/__init__.py +185 -0
  12. kurrentdbclient/instrumentation/opentelemetry/attributes.py +20 -0
  13. kurrentdbclient/instrumentation/opentelemetry/grpc.py +165 -0
  14. kurrentdbclient/instrumentation/opentelemetry/package.py +2 -0
  15. kurrentdbclient/instrumentation/opentelemetry/spanners.py +1097 -0
  16. kurrentdbclient/instrumentation/opentelemetry/utils.py +199 -0
  17. kurrentdbclient/instrumentation/opentelemetry/version.py +2 -0
  18. kurrentdbclient/persistent.py +1982 -0
  19. kurrentdbclient/projections.py +735 -0
  20. kurrentdbclient/protos/Grpc/cluster_pb2.py +92 -0
  21. kurrentdbclient/protos/Grpc/cluster_pb2.pyi +765 -0
  22. kurrentdbclient/protos/Grpc/cluster_pb2_grpc.py +514 -0
  23. kurrentdbclient/protos/Grpc/code_pb2.py +37 -0
  24. kurrentdbclient/protos/Grpc/code_pb2.pyi +357 -0
  25. kurrentdbclient/protos/Grpc/code_pb2_grpc.py +24 -0
  26. kurrentdbclient/protos/Grpc/gossip_pb2.py +46 -0
  27. kurrentdbclient/protos/Grpc/gossip_pb2.pyi +126 -0
  28. kurrentdbclient/protos/Grpc/gossip_pb2_grpc.py +98 -0
  29. kurrentdbclient/protos/Grpc/persistent_pb2.py +140 -0
  30. kurrentdbclient/protos/Grpc/persistent_pb2.pyi +1135 -0
  31. kurrentdbclient/protos/Grpc/persistent_pb2_grpc.py +399 -0
  32. kurrentdbclient/protos/Grpc/projections_pb2.py +99 -0
  33. kurrentdbclient/protos/Grpc/projections_pb2.pyi +558 -0
  34. kurrentdbclient/protos/Grpc/projections_pb2_grpc.py +485 -0
  35. kurrentdbclient/protos/Grpc/shared_pb2.py +62 -0
  36. kurrentdbclient/protos/Grpc/shared_pb2.pyi +218 -0
  37. kurrentdbclient/protos/Grpc/shared_pb2_grpc.py +24 -0
  38. kurrentdbclient/protos/Grpc/status_pb2.py +39 -0
  39. kurrentdbclient/protos/Grpc/status_pb2.pyi +67 -0
  40. kurrentdbclient/protos/Grpc/status_pb2_grpc.py +24 -0
  41. kurrentdbclient/protos/Grpc/streams_pb2.py +132 -0
  42. kurrentdbclient/protos/Grpc/streams_pb2.pyi +1038 -0
  43. kurrentdbclient/protos/Grpc/streams_pb2_grpc.py +269 -0
  44. kurrentdbclient/py.typed +0 -0
  45. kurrentdbclient/streams.py +1400 -0
  46. kurrentdbclient-0.3.dist-info/LICENSE +29 -0
  47. kurrentdbclient-0.3.dist-info/METADATA +3769 -0
  48. kurrentdbclient-0.3.dist-info/RECORD +49 -0
  49. 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)