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,371 @@
1
+ # -*- coding: utf-8 -*-
2
+ from typing import Any, Dict, Optional, Sequence
3
+ from urllib.parse import ParseResult, parse_qs, urlparse
4
+ from uuid import uuid4
5
+
6
+ URI_SCHEME_ESDB = "esdb"
7
+ URI_SCHEME_ESDB_DISCOVER = "esdb+discover"
8
+ URI_SCHEME_KURRENTDB = "kurrentdb"
9
+ URI_SCHEME_KURRENTDB_DISCOVER = "kurrentdb+discover"
10
+ URI_SCHEME_KDB = "kdb"
11
+ URI_SCHEME_KDB_DISCOVER = "kdb+discover"
12
+
13
+ URI_SCHEMES_NON_DISCOVER = [
14
+ URI_SCHEME_ESDB,
15
+ URI_SCHEME_KURRENTDB,
16
+ URI_SCHEME_KDB,
17
+ ]
18
+
19
+ URI_SCHEMES_DISCOVER = [
20
+ URI_SCHEME_ESDB_DISCOVER,
21
+ URI_SCHEME_KURRENTDB_DISCOVER,
22
+ URI_SCHEME_KDB_DISCOVER,
23
+ ]
24
+
25
+ URI_SCHEMES_ALL = URI_SCHEMES_NON_DISCOVER + URI_SCHEMES_DISCOVER
26
+
27
+ NODE_PREFERENCE_LEADER = "leader"
28
+ NODE_PREFERENCE_FOLLOWER = "follower"
29
+ NODE_PREFERENCE_RANDOM = "random"
30
+ NODE_PREFERENCE_REPLICA = "readonlyreplica"
31
+ VALID_NODE_PREFERENCES = [
32
+ NODE_PREFERENCE_LEADER,
33
+ NODE_PREFERENCE_FOLLOWER,
34
+ NODE_PREFERENCE_RANDOM,
35
+ NODE_PREFERENCE_REPLICA,
36
+ ]
37
+ VALID_CONNECTION_QUERY_STRING_FIELDS = [
38
+ "Tls",
39
+ "ConnectionName",
40
+ "MaxDiscoverAttempts",
41
+ "DiscoveryInterval",
42
+ "GossipTimeout",
43
+ "NodePreference",
44
+ "TlsVerifyCert",
45
+ "DefaultDeadline",
46
+ "KeepAliveInterval",
47
+ "KeepAliveTimeout",
48
+ "TlsCaFile",
49
+ "UserCertFile",
50
+ "UserKeyFile",
51
+ ]
52
+
53
+
54
+ class ConnectionOptions:
55
+ __slots__ = [f"_{s}" for s in VALID_CONNECTION_QUERY_STRING_FIELDS]
56
+
57
+ def __init__(self, query: str):
58
+ # Parse query string (case insensitivity, assume single values).
59
+ options = {k.upper(): v[0] for k, v in parse_qs(query).items()}
60
+
61
+ self._validate_field_names(options)
62
+ self._set_Tls(options)
63
+ self._set_ConnectionName(options)
64
+ self._set_MaxDiscoverAttempts(options)
65
+ self._set_DiscoveryInterval(options)
66
+ self._set_GossipTimeout(options)
67
+ self._set_NodePreference(options)
68
+ self._set_TlsVerifyCert(options)
69
+ self._set_DefaultDeadline(options)
70
+ self._set_KeepAliveInterval(options)
71
+ self._set_KeepAliveTimeout(options)
72
+ self._set_TlsCaFile(options)
73
+ self._set_UserCertFile(options)
74
+ self._set_UserKeyFile(options)
75
+
76
+ @staticmethod
77
+ def _validate_field_names(options: Dict[str, Any]) -> None:
78
+ valid_fields = [s.upper() for s in VALID_CONNECTION_QUERY_STRING_FIELDS]
79
+ invalid_fields = []
80
+ for field in options.keys():
81
+ if field not in valid_fields:
82
+ invalid_fields.append(field)
83
+ if len(invalid_fields) > 0:
84
+ plural = "s" if len(invalid_fields) > 1 else ""
85
+ joined_fields = ", ".join(invalid_fields)
86
+ raise ValueError(
87
+ f"Unknown field{plural} in connection query string: {joined_fields}"
88
+ )
89
+
90
+ def _set_Tls(self, options: Dict[str, Any]) -> None:
91
+ _Tls = options.get("Tls".upper())
92
+ if _Tls is None:
93
+ self._Tls = True
94
+ else:
95
+ validTlsValues = ["true", "false"]
96
+ if _Tls.lower() not in validTlsValues:
97
+ raise ValueError(f"'{_Tls}' not one of: {', '.join(validTlsValues)}")
98
+ elif _Tls.lower() == "true":
99
+ self._Tls = True
100
+ else:
101
+ self._Tls = False
102
+
103
+ def _set_ConnectionName(self, options: Dict[str, Any]) -> None:
104
+ _ConnectionName = options.get("ConnectionName".upper())
105
+ if _ConnectionName is None:
106
+ self._ConnectionName = str(uuid4())
107
+ else:
108
+ self._ConnectionName = _ConnectionName
109
+
110
+ def _set_MaxDiscoverAttempts(self, options: Dict[str, Any]) -> None:
111
+ _MaxDiscoverAttempts = options.get("MaxDiscoverAttempts".upper())
112
+ if _MaxDiscoverAttempts is None:
113
+ self._MaxDiscoverAttempts = 10
114
+ else:
115
+ self._MaxDiscoverAttempts = int(_MaxDiscoverAttempts)
116
+
117
+ def _set_DiscoveryInterval(self, options: Dict[str, Any]) -> None:
118
+ _DiscoveryInterval = options.get("DiscoveryInterval".upper())
119
+ if _DiscoveryInterval is None:
120
+ self._DiscoveryInterval = 100
121
+ else:
122
+ self._DiscoveryInterval = int(_DiscoveryInterval)
123
+
124
+ def _set_GossipTimeout(self, options: Dict[str, Any]) -> None:
125
+ _GossipTimeout = options.get("GossipTimeout".upper())
126
+ if _GossipTimeout is None:
127
+ self._GossipTimeout = 5
128
+ else:
129
+ self._GossipTimeout = int(_GossipTimeout)
130
+
131
+ def _set_NodePreference(self, options: Dict[str, Any]) -> None:
132
+ _NodePreference = options.get("NodePreference".upper())
133
+ if _NodePreference is None:
134
+ self._NodePreference = NODE_PREFERENCE_LEADER
135
+ else:
136
+ if _NodePreference.lower() not in VALID_NODE_PREFERENCES:
137
+ raise ValueError(
138
+ f"'{_NodePreference}' not one of:"
139
+ f" {', '.join(VALID_NODE_PREFERENCES)}"
140
+ )
141
+ self._NodePreference = _NodePreference.lower()
142
+
143
+ def _set_TlsVerifyCert(self, options: Dict[str, Any]) -> None:
144
+ _TlsVerifyCert = options.get("TlsVerifyCert".upper())
145
+ if _TlsVerifyCert is None:
146
+ self._TlsVerifyCert = True
147
+ else:
148
+ validTlsVerifyCertValues = ["true", "false"]
149
+ if _TlsVerifyCert.lower() not in validTlsVerifyCertValues:
150
+ raise ValueError(
151
+ f"'{_TlsVerifyCert}' not one of:"
152
+ f" {', '.join(validTlsVerifyCertValues)}"
153
+ )
154
+ elif _TlsVerifyCert.lower() == "true":
155
+ self._TlsVerifyCert = True
156
+ else:
157
+ self._TlsVerifyCert = False
158
+
159
+ def _set_DefaultDeadline(self, options: Dict[str, Any]) -> None:
160
+ _DefaultDeadline = options.get("DefaultDeadline".upper())
161
+ if _DefaultDeadline is None:
162
+ self._DefaultDeadline: Optional[int] = None
163
+ else:
164
+ self._DefaultDeadline = int(_DefaultDeadline)
165
+
166
+ def _set_KeepAliveInterval(self, options: Dict[str, Any]) -> None:
167
+ _KeepAliveInterval = options.get("KeepAliveInterval".upper())
168
+ if _KeepAliveInterval is None:
169
+ self._KeepAliveInterval: Optional[int] = None
170
+ else:
171
+ self._KeepAliveInterval = int(_KeepAliveInterval)
172
+
173
+ def _set_KeepAliveTimeout(self, options: Dict[str, Any]) -> None:
174
+ _KeepAliveTimeout = options.get("KeepAliveTimeout".upper())
175
+ if _KeepAliveTimeout is None:
176
+ self._KeepAliveTimeout: Optional[int] = None
177
+ else:
178
+ self._KeepAliveTimeout = int(_KeepAliveTimeout)
179
+
180
+ def _set_TlsCaFile(self, options: Dict[str, Any]) -> None:
181
+ _TlsCaFile = options.get("TlsCaFile".upper())
182
+ if _TlsCaFile is None:
183
+ self._TlsCaFile: Optional[str] = None
184
+ else:
185
+ self._TlsCaFile = str(_TlsCaFile)
186
+
187
+ def _set_UserCertFile(self, options: Dict[str, Any]) -> None:
188
+ _UserCertFile = options.get("UserCertFile".upper())
189
+ if _UserCertFile is None:
190
+ self._UserCertFile: Optional[str] = None
191
+ else:
192
+ self._UserCertFile = str(_UserCertFile)
193
+
194
+ def _set_UserKeyFile(self, options: Dict[str, Any]) -> None:
195
+ _UserKeyFile = options.get("UserKeyFile".upper())
196
+ if _UserKeyFile is None:
197
+ self._UserKeyFile: Optional[str] = None
198
+ else:
199
+ self._UserKeyFile = str(_UserKeyFile)
200
+
201
+ @property
202
+ def Tls(self) -> bool:
203
+ """
204
+ Controls whether client will use a secure channel (has to match server).
205
+
206
+ Valid values in URI: 'true', 'false'.
207
+ """
208
+ return self._Tls
209
+
210
+ @property
211
+ def ConnectionName(self) -> str:
212
+ """
213
+ This value is sent as header 'connection-name' in all calls to server.
214
+
215
+ Defaults to a new version 4 UUID string.
216
+ """
217
+ return self._ConnectionName
218
+
219
+ @property
220
+ def MaxDiscoverAttempts(self) -> int:
221
+ """
222
+ Number of attempts to connect to gossip before giving up.
223
+ """
224
+ return self._MaxDiscoverAttempts
225
+
226
+ @property
227
+ def DiscoveryInterval(self) -> int:
228
+ """
229
+ How long to wait (in milliseconds) between gossip retries.
230
+ """
231
+ return self._DiscoveryInterval
232
+
233
+ @property
234
+ def GossipTimeout(self) -> int:
235
+ """
236
+ How long to wait (in seconds) for a response to a request to gossip API.
237
+ """
238
+ return self._GossipTimeout
239
+
240
+ @property
241
+ def NodePreference(
242
+ self,
243
+ ) -> str:
244
+ """
245
+ Controls whether requests are directed to another node.
246
+
247
+ Value values: 'leader', 'follower', 'random', 'readonlyreplica'.
248
+ """
249
+ return self._NodePreference
250
+
251
+ @property
252
+ def TlsVerifyCert(self) -> bool:
253
+ """
254
+ Controls whether certificate is verified.
255
+
256
+ Valid values in URI: 'true', 'false'.
257
+ """
258
+ return self._TlsVerifyCert
259
+
260
+ @property
261
+ def DefaultDeadline(self) -> Optional[int]:
262
+ """
263
+ Default deadline (in seconds) for calls to the server that write data.
264
+ """
265
+ return self._DefaultDeadline
266
+
267
+ @property
268
+ def KeepAliveInterval(self) -> Optional[int]:
269
+ """
270
+ gRPC "keep alive" interval (in milliseconds).
271
+ """
272
+ return self._KeepAliveInterval
273
+
274
+ @property
275
+ def KeepAliveTimeout(self) -> Optional[int]:
276
+ """
277
+ gRPC "keep alive timeout" (in milliseconds).
278
+ """
279
+ return self._KeepAliveTimeout
280
+
281
+ @property
282
+ def TlsCaFile(self) -> Optional[str]:
283
+ """
284
+ Path to file containing root CA certificate(s) to verify server.
285
+ """
286
+ return self._TlsCaFile
287
+
288
+ @property
289
+ def UserCertFile(self) -> Optional[str]:
290
+ """
291
+ Path to file containing user X.509 certificate.
292
+ """
293
+ return self._UserCertFile
294
+
295
+ @property
296
+ def UserKeyFile(self) -> Optional[str]:
297
+ """
298
+ Path to file containing user X.509 key.
299
+ """
300
+ return self._UserKeyFile
301
+
302
+
303
+ class ConnectionSpec:
304
+ __slots__ = [
305
+ "_uri",
306
+ "_scheme",
307
+ "_netloc",
308
+ "_username",
309
+ "_password",
310
+ "_targets",
311
+ "_options",
312
+ ]
313
+
314
+ def __init__(self, uri: Optional[str] = None):
315
+ self._uri = uri or ""
316
+ parse_result: ParseResult = urlparse(self._uri)
317
+ if parse_result.scheme not in URI_SCHEMES_ALL:
318
+ raise ValueError(
319
+ f"Invalid URI scheme: '{parse_result.scheme}' not in:"
320
+ f" {', '.join(URI_SCHEMES_ALL)}: {uri}"
321
+ )
322
+ self._scheme = parse_result.scheme
323
+ self._netloc = parse_result.netloc
324
+ self._username = parse_result.username
325
+ self._password = parse_result.password
326
+ if "@" in self._netloc:
327
+ _, _, targets = self._netloc.partition("@")
328
+ else:
329
+ targets = self._netloc
330
+ self._targets = [t.strip() for t in targets.split(",") if t.strip()]
331
+
332
+ if len(self._targets) == 0:
333
+ raise ValueError(f"No targets specified: {uri}")
334
+ if self._scheme in URI_SCHEMES_DISCOVER:
335
+ if len(self._targets) > 1:
336
+ raise ValueError(f"More than one target specified: {uri}")
337
+
338
+ for i, target in enumerate(self._targets):
339
+ host, _, port = target.partition(":")
340
+ if port == "":
341
+ port = "2113"
342
+ self._targets[i] = f"{host}:{port}"
343
+
344
+ self._options = ConnectionOptions(parse_result.query)
345
+ if self._options.Tls is True:
346
+ if not self._username or not self._password:
347
+ raise ValueError(f"Username and password are required: {uri}")
348
+
349
+ @property
350
+ def uri(self) -> str:
351
+ return self._uri
352
+
353
+ @property
354
+ def scheme(self) -> str:
355
+ return self._scheme
356
+
357
+ @property
358
+ def username(self) -> Optional[str]:
359
+ return self._username
360
+
361
+ @property
362
+ def password(self) -> Optional[str]:
363
+ return self._password
364
+
365
+ @property
366
+ def targets(self) -> Sequence[str]:
367
+ return self._targets
368
+
369
+ @property
370
+ def options(self) -> ConnectionOptions:
371
+ return self._options
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime
4
+ from typing import Optional
5
+ from uuid import UUID, uuid4
6
+
7
+ from typing_extensions import Literal
8
+
9
+ ContentType = Literal["application/json", "application/octet-stream"]
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class NewEvent:
14
+ """
15
+ Encapsulates event data to be recorded in KurrentDB.
16
+ """
17
+
18
+ type: str
19
+ data: bytes
20
+ metadata: bytes = b""
21
+ content_type: ContentType = "application/json"
22
+ id: UUID = field(default_factory=uuid4)
23
+
24
+ def __eq__(self, other: object) -> bool:
25
+ return isinstance(other, (NewEvent, RecordedEvent)) and self.id == other.id
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RecordedEvent:
30
+ """
31
+ Encapsulates event data that has been recorded in KurrentDB.
32
+ """
33
+
34
+ type: str
35
+ data: bytes
36
+ metadata: bytes
37
+ content_type: str
38
+ id: UUID
39
+ stream_name: str
40
+ stream_position: int
41
+ commit_position: Optional[int]
42
+ prepare_position: Optional[int]
43
+ recorded_at: Optional[datetime] = None
44
+ link: Optional["RecordedEvent"] = None
45
+ retry_count: Optional[int] = None
46
+
47
+ @property
48
+ def ack_id(self) -> UUID:
49
+ if self.link is not None:
50
+ return self.link.id
51
+ else:
52
+ return self.id
53
+
54
+ @property
55
+ def is_system_event(self) -> bool:
56
+ return self.type.startswith("$")
57
+
58
+ @property
59
+ def is_link_event(self) -> bool:
60
+ return self.type == "$>"
61
+
62
+ @property
63
+ def is_resolved_event(self) -> bool:
64
+ return self.link is not None
65
+
66
+ @property
67
+ def is_checkpoint(self) -> bool:
68
+ return False
69
+
70
+ @property
71
+ def is_caught_up(self) -> bool:
72
+ return False
73
+
74
+ # @property
75
+ # def is_fell_behind(self) -> bool:
76
+ # return False
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class Checkpoint(RecordedEvent):
81
+ CHECKPOINT_ID = UUID("00000000-0000-0000-0000-000000000000")
82
+
83
+ def __init__(self, commit_position: int, prepare_position: int) -> None:
84
+ super().__init__(
85
+ id=Checkpoint.CHECKPOINT_ID,
86
+ type="",
87
+ data=b"",
88
+ content_type="",
89
+ metadata=b"",
90
+ stream_name="",
91
+ stream_position=0,
92
+ commit_position=commit_position,
93
+ prepare_position=prepare_position,
94
+ )
95
+
96
+ @property
97
+ def is_checkpoint(self) -> bool:
98
+ return True
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class CaughtUp(RecordedEvent):
103
+ CAUGHT_UP_ID = UUID("00000000-0000-0000-0000-000000000000")
104
+
105
+ def __init__(self) -> None:
106
+ super().__init__(
107
+ id=CaughtUp.CAUGHT_UP_ID,
108
+ type="",
109
+ data=b"",
110
+ content_type="",
111
+ metadata=b"",
112
+ stream_name="",
113
+ stream_position=0,
114
+ commit_position=0,
115
+ prepare_position=0,
116
+ )
117
+
118
+ @property
119
+ def is_caught_up(self) -> bool:
120
+ return True
121
+
122
+
123
+ # @dataclass(frozen=True)
124
+ # class FellBehind(RecordedEvent):
125
+ # FELL_BEHIND_ID = UUID("00000000-0000-0000-0000-000000000000")
126
+ #
127
+ # def __init__(self) -> None:
128
+ # super().__init__(
129
+ # id=FellBehind.FELL_BEHIND_ID,
130
+ # type="",
131
+ # data=b"",
132
+ # content_type="",
133
+ # metadata=b"",
134
+ # stream_name="",
135
+ # stream_position=0,
136
+ # commit_position=0,
137
+ # )
138
+ #
139
+ # @property
140
+ # def is_fell_behind(self) -> bool:
141
+ # return True