tigerbeetle 0.16.16__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.
tigerbeetle/client.py ADDED
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import ctypes
5
+ import logging
6
+ import os
7
+ import threading
8
+ import time
9
+ from collections.abc import Callable # noqa: TCH003
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from . import bindings
14
+ from .lib import tb_assert
15
+
16
+ logger = logging.getLogger("tigerbeetle")
17
+
18
+
19
+ class AtomicInteger:
20
+ def __init__(self, value=0):
21
+ self._value = value
22
+ self._lock = threading.Lock()
23
+
24
+ def increment(self):
25
+ with self._lock:
26
+ self._value += 1
27
+ return self._value
28
+
29
+
30
+ @dataclass
31
+ class CompletionContextSync:
32
+ event: threading.Event
33
+
34
+
35
+ @dataclass
36
+ class CompletionContextAsync:
37
+ loop: asyncio.AbstractEventLoop
38
+ event: asyncio.Event
39
+
40
+
41
+ @dataclass
42
+ class InflightPacket:
43
+ packet: bindings.CPacket
44
+ response: Any
45
+ operation: bindings.Operation
46
+ c_event_type: Any
47
+ c_result_type: Any
48
+ on_completion: Callable | None
49
+ on_completion_context: CompletionContextSync | CompletionContextAsync | None
50
+
51
+
52
+
53
+ def id() -> int:
54
+ """
55
+ Generates a Universally Unique and Sortable Identifier as a 128-bit integer. Based on ULIDs.
56
+ """
57
+ time_ms = time.time_ns() // (1000 * 1000)
58
+
59
+ # Ensure time_ms monotonically increases.
60
+ time_ms_last = getattr(id, "_time_ms_last", 0)
61
+ if time_ms <= time_ms_last:
62
+ time_ms = time_ms_last
63
+ else:
64
+ id._time_ms_last = time_ms
65
+
66
+ randomness = os.urandom(10)
67
+
68
+ return int.from_bytes(
69
+ time_ms.to_bytes(6, "big") + randomness,
70
+ "big",
71
+ )
72
+
73
+
74
+ amount_max = (2 ** 128) - 1
75
+
76
+
77
+ class InitError(Exception):
78
+ pass
79
+
80
+ class PacketError(Exception):
81
+ pass
82
+
83
+
84
+ class Client:
85
+ _clients: dict[int, Any] = {}
86
+ _counter = AtomicInteger()
87
+
88
+ def __init__(self, cluster_id: int, replica_addresses: str):
89
+ self._client_key = Client._counter.increment()
90
+ self._client = bindings.Client()
91
+
92
+ self._inflight_packets: dict[int, InflightPacket] = {}
93
+
94
+ init_status = bindings.tb_client_init(
95
+ ctypes.byref(self._client),
96
+ cluster_id,
97
+ replica_addresses.encode("ascii"),
98
+ len(replica_addresses),
99
+ self._client_key,
100
+ self._c_on_completion
101
+ )
102
+ if init_status != bindings.Status.SUCCESS:
103
+ raise InitError(init_status)
104
+
105
+ Client._clients[self._client_key] = self
106
+
107
+
108
+ def _acquire_packet(self, operation: bindings.Operation, operations: Any,
109
+ c_event_type: Any, c_result_type: Any) -> InflightPacket:
110
+ packet = bindings.CPacket()
111
+ packet.next = None
112
+ packet.user_data = Client._counter.increment()
113
+ packet.operation = operation
114
+ packet.status = bindings.PacketStatus.OK
115
+
116
+ operations_array_type = c_event_type * len(operations)
117
+ operations_array = operations_array_type(*map(c_event_type.from_param, operations))
118
+
119
+ packet.data_size = ctypes.sizeof(operations_array)
120
+ packet.data = ctypes.cast(operations_array, ctypes.c_void_p)
121
+
122
+ return InflightPacket(
123
+ packet=packet,
124
+ response=None,
125
+ on_completion=None,
126
+ on_completion_context=None,
127
+ operation=operation,
128
+ c_event_type=c_event_type,
129
+ c_result_type=c_result_type)
130
+
131
+ def close(self):
132
+ bindings.tb_client_deinit(self._client)
133
+ tb_assert(self._client is not None)
134
+ tb_assert(len(self._inflight_packets) == 0)
135
+ del Client._clients[self._client_key]
136
+
137
+ @staticmethod
138
+ @bindings.OnCompletion
139
+ def _c_on_completion(completion_ctx, tb_client, packet, timestamp, bytes_ptr, len_):
140
+ """
141
+ Invoked in a separate thread
142
+ """
143
+ self: Client = Client._clients[completion_ctx]
144
+ tb_assert(self._client.value == tb_client)
145
+
146
+ packet = ctypes.cast(packet, ctypes.POINTER(bindings.CPacket))
147
+ inflight_packet = self._inflight_packets[packet[0].user_data]
148
+
149
+ if packet[0].status != bindings.PacketStatus.OK.value:
150
+ inflight_packet.response = PacketError(repr(bindings.PacketStatus(packet[0].status)))
151
+ tb_assert(inflight_packet.on_completion is not None)
152
+ inflight_packet.on_completion(inflight_packet)
153
+ return
154
+
155
+ c_result_type = inflight_packet.c_result_type
156
+ tb_assert(len_ % ctypes.sizeof(c_result_type) == 0)
157
+
158
+ # The memory referenced in bytes_ptr is only valid for the duration of this callback. Copy
159
+ # it to a fresh, Python owned buffer and do the conversion from the raw C type to the Python
160
+ # dataclass.
161
+ results_slice = ctypes.cast(
162
+ bytes_ptr,
163
+ ctypes.POINTER(c_result_type)
164
+ )[0:(len_ // ctypes.sizeof(c_result_type))]
165
+ results = [result.to_python() for result in results_slice]
166
+
167
+ inflight_packet.response = results
168
+
169
+ tb_assert(inflight_packet.on_completion is not None)
170
+ inflight_packet.on_completion(inflight_packet)
171
+
172
+ def __enter__(self):
173
+ return self
174
+
175
+ def __exit__(self, exc_type, exc_val, exc_tb):
176
+ self.close()
177
+
178
+
179
+ class ClientSync(Client, bindings.StateMachineMixin):
180
+ def _on_completion(self, inflight_packet):
181
+ inflight_packet.on_completion_context.event.set()
182
+
183
+ def _submit(self, operation: bindings.Operation, operations: list[Any],
184
+ c_event_type: Any, c_result_type: Any):
185
+ inflight_packet = self._acquire_packet(operation, operations, c_event_type, c_result_type)
186
+ self._inflight_packets[inflight_packet.packet.user_data] = inflight_packet
187
+
188
+ inflight_packet.on_completion = self._on_completion
189
+ inflight_packet.on_completion_context = CompletionContextSync(event=threading.Event())
190
+
191
+ bindings.tb_client_submit(self._client, ctypes.byref(inflight_packet.packet))
192
+ inflight_packet.on_completion_context.event.wait()
193
+
194
+ del self._inflight_packets[inflight_packet.packet.user_data]
195
+
196
+ if isinstance(inflight_packet.response, Exception):
197
+ raise inflight_packet.response
198
+
199
+ return inflight_packet.response
200
+
201
+
202
+ class ClientAsync(Client, bindings.AsyncStateMachineMixin):
203
+ def _on_completion(self, inflight_packet):
204
+ """
205
+ Called by Client._c_on_completion, which itself is called from a different thread. Use
206
+ `call_soon_threadsafe` to return to the thread of the event loop the request was invoked
207
+ from, so _trigger_event() can trigger the async event and allow the client to progress.
208
+ """
209
+ inflight_packet.on_completion_context.loop.call_soon_threadsafe(
210
+ self._trigger_event,
211
+ inflight_packet
212
+ )
213
+
214
+ def _trigger_event(self, inflight_packet):
215
+ inflight_packet.on_completion_context.event.set()
216
+
217
+ async def _submit(self, operation: bindings.Operation, operations: Any,
218
+ c_event_type: Any, c_result_type: Any):
219
+ inflight_packet = self._acquire_packet(operation, operations, c_event_type, c_result_type)
220
+ self._inflight_packets[inflight_packet.packet.user_data] = inflight_packet
221
+
222
+ inflight_packet.on_completion = self._on_completion
223
+ inflight_packet.on_completion_context = CompletionContextAsync(
224
+ loop=asyncio.get_event_loop(),
225
+ event=asyncio.Event()
226
+ )
227
+
228
+ bindings.tb_client_submit(self._client, ctypes.byref(inflight_packet.packet))
229
+ await inflight_packet.on_completion_context.event.wait()
230
+
231
+ del self._inflight_packets[inflight_packet.packet.user_data]
232
+
233
+ if isinstance(inflight_packet.response, Exception):
234
+ raise inflight_packet.response
235
+
236
+ return inflight_packet.response
tigerbeetle/lib.py ADDED
@@ -0,0 +1,88 @@
1
+ import ctypes
2
+ import dataclasses
3
+ import platform
4
+ from pathlib import Path
5
+
6
+
7
+ class NativeError(Exception):
8
+ pass
9
+
10
+ class IntegerOverflowError(ValueError):
11
+ pass
12
+
13
+
14
+ def _load_tbclient():
15
+ prefix = ""
16
+ arch = ""
17
+ system = ""
18
+ linux_libc = ""
19
+ suffix = ""
20
+
21
+ platform_machine = platform.machine().lower()
22
+
23
+ if platform_machine == "x86_64" or platform_machine == "amd64":
24
+ arch = "x86_64"
25
+ elif platform_machine == "aarch64" or platform_machine == "arm64":
26
+ arch = "aarch64"
27
+ else:
28
+ raise NativeError("Unsupported machine: " + platform.machine())
29
+
30
+ if platform.system() == "Linux":
31
+ prefix = "lib"
32
+ system = "linux"
33
+ suffix = ".so"
34
+ libc = platform.libc_ver()[0]
35
+ if libc == "glibc":
36
+ linux_libc = "-gnu.2.27"
37
+ elif libc == "musl":
38
+ linux_libc = "-musl"
39
+ else:
40
+ raise NativeError("Unsupported libc: " + libc)
41
+ elif platform.system() == "Darwin":
42
+ prefix = "lib"
43
+ system = "macos"
44
+ suffix = ".dylib"
45
+ elif platform.system() == "Windows":
46
+ system = "windows"
47
+ suffix = ".dll"
48
+ else:
49
+ raise NativeError("Unsupported system: " + platform.system())
50
+
51
+ source_path = Path(__file__)
52
+ source_dir = source_path.parent
53
+ library_path = source_dir / "lib" / f"{arch}-{system}{linux_libc}" / f"{prefix}tb_client{suffix}"
54
+ return ctypes.CDLL(str(library_path))
55
+
56
+
57
+ def validate_uint(*, bits: int, name: str, number: int):
58
+ if number > 2**bits - 1:
59
+ raise IntegerOverflowError(f"{name}=={number} is too large to fit in {bits} bits")
60
+ if number < 0:
61
+ raise IntegerOverflowError(f"{name}=={number} cannot be negative")
62
+
63
+
64
+ class c_uint128(ctypes.Structure): # noqa: N801
65
+ _fields_ = [("_low", ctypes.c_uint64), ("_high", ctypes.c_uint64)] # noqa: RUF012
66
+
67
+ @classmethod
68
+ def from_param(cls, obj):
69
+ return cls(_high=obj >> 64, _low=obj & 0xffffffffffffffff)
70
+
71
+ def to_python(self):
72
+ return self._high << 64 | self._low
73
+
74
+
75
+ # Use slots=True if the version of Python is new enough (3.10+) to support it.
76
+ try:
77
+ dataclass = dataclasses.dataclass(slots=True)
78
+ except TypeError:
79
+ dataclass = dataclasses.dataclass()
80
+
81
+ def tb_assert(value):
82
+ """
83
+ Python's built-in assert can be silently disabled if Python is run with -O.
84
+ """
85
+ if not value:
86
+ raise AssertionError()
87
+
88
+ tbclient = _load_tbclient()