plasmite 1.3.0__py3-none-win_amd64.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.
- plasmite/__init__.py +1454 -0
- plasmite/_cli.py +89 -0
- plasmite/_native/plasmite.dll +0 -0
- plasmite/_native/plasmite.exe +0 -0
- plasmite-1.3.0.dist-info/METADATA +201 -0
- plasmite-1.3.0.dist-info/RECORD +11 -0
- plasmite-1.3.0.dist-info/WHEEL +5 -0
- plasmite-1.3.0.dist-info/entry_points.txt +3 -0
- plasmite-1.3.0.dist-info/licenses/LICENSE +21 -0
- plasmite-1.3.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +18 -0
- plasmite-1.3.0.dist-info/top_level.txt +1 -0
plasmite/__init__.py
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Provide Python bindings for the libplasmite C ABI (v0).
|
|
3
|
+
Key Exports: Client, Pool, Stream, Durability, ErrorKind, PlasmiteError.
|
|
4
|
+
Role: Minimal, ergonomic wrapper around include/plasmite.h for Python users.
|
|
5
|
+
Invariants: JSON bytes in/out; native operations and close serialize per handle.
|
|
6
|
+
Invariants: Errors preserve stable kinds and context fields.
|
|
7
|
+
Notes: Uses ctypes and links to libplasmite resolved at runtime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from ctypes import (
|
|
14
|
+
CDLL,
|
|
15
|
+
POINTER,
|
|
16
|
+
Structure,
|
|
17
|
+
byref,
|
|
18
|
+
c_char_p,
|
|
19
|
+
c_int,
|
|
20
|
+
c_size_t,
|
|
21
|
+
c_uint32,
|
|
22
|
+
c_uint64,
|
|
23
|
+
c_uint8,
|
|
24
|
+
c_void_p,
|
|
25
|
+
cast,
|
|
26
|
+
sizeof,
|
|
27
|
+
)
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from enum import IntEnum
|
|
30
|
+
from functools import wraps
|
|
31
|
+
from threading import Lock, RLock
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
import sys
|
|
37
|
+
import time as _time
|
|
38
|
+
from typing import Any, Generator, Iterable, Optional
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ErrorKind(IntEnum):
|
|
42
|
+
INTERNAL = 1
|
|
43
|
+
USAGE = 2
|
|
44
|
+
NOT_FOUND = 3
|
|
45
|
+
ALREADY_EXISTS = 4
|
|
46
|
+
BUSY = 5
|
|
47
|
+
PERMISSION = 6
|
|
48
|
+
CORRUPT = 7
|
|
49
|
+
IO = 8
|
|
50
|
+
RETENTION_GAP = 9
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Durability(IntEnum):
|
|
54
|
+
FAST = 0
|
|
55
|
+
FLUSH = 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PlasmiteError(RuntimeError):
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
kind: ErrorKind,
|
|
62
|
+
message: str,
|
|
63
|
+
path: str | None = None,
|
|
64
|
+
seq: int | None = None,
|
|
65
|
+
offset: int | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
super().__init__(message)
|
|
68
|
+
self.kind = kind
|
|
69
|
+
self.path = path
|
|
70
|
+
self.seq = seq
|
|
71
|
+
self.offset = offset
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NotFoundError(PlasmiteError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AlreadyExistsError(PlasmiteError):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BusyError(PlasmiteError):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PermissionDeniedError(PlasmiteError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CorruptError(PlasmiteError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class IoError(PlasmiteError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UsageError(PlasmiteError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class InternalError(PlasmiteError):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RetentionGapError(PlasmiteError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class MessageMeta:
|
|
112
|
+
tags: list[str]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class Message:
|
|
117
|
+
seq: int
|
|
118
|
+
time: datetime
|
|
119
|
+
time_rfc3339: str
|
|
120
|
+
data: Any
|
|
121
|
+
meta: MessageMeta
|
|
122
|
+
raw: bytes
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def tags(self) -> list[str]:
|
|
126
|
+
return self.meta.tags
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class plsm_client_t(Structure):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class plsm_pool_t(Structure):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class plsm_stream_t(Structure):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class plsm_lite3_stream_t(Structure):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class plsm_udp_client_t(Structure):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class plsm_udp_pool_t(Structure):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class plsm_udp_follow_t(Structure):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class plsm_buf_t(Structure):
|
|
158
|
+
_fields_ = [
|
|
159
|
+
("data", c_void_p),
|
|
160
|
+
("len", c_size_t),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class plsm_udp_event_t(Structure):
|
|
165
|
+
_fields_ = [
|
|
166
|
+
("kind", c_uint32),
|
|
167
|
+
("reason", c_uint32),
|
|
168
|
+
("seq", c_uint64),
|
|
169
|
+
("timestamp_ns", c_uint64),
|
|
170
|
+
("first", c_uint64),
|
|
171
|
+
("last", c_uint64),
|
|
172
|
+
("start_seq", c_uint64),
|
|
173
|
+
("next_seq", c_uint64),
|
|
174
|
+
("end_seq", c_uint64),
|
|
175
|
+
("has_end", c_uint8),
|
|
176
|
+
("terminal", c_uint8),
|
|
177
|
+
("retention_gap", c_uint8),
|
|
178
|
+
("reserved", c_uint8),
|
|
179
|
+
("payload", plsm_buf_t),
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class plsm_lite3_frame_t(Structure):
|
|
184
|
+
_fields_ = [
|
|
185
|
+
("seq", c_uint64),
|
|
186
|
+
("timestamp_ns", c_uint64),
|
|
187
|
+
("flags", c_uint32),
|
|
188
|
+
("payload", plsm_buf_t),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class plsm_error_t(Structure):
|
|
193
|
+
_fields_ = [
|
|
194
|
+
("kind", c_int),
|
|
195
|
+
("message", c_char_p),
|
|
196
|
+
("path", c_char_p),
|
|
197
|
+
("seq", c_uint64),
|
|
198
|
+
("offset", c_uint64),
|
|
199
|
+
("has_seq", c_uint8),
|
|
200
|
+
("has_offset", c_uint8),
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class plsm_stream_options_t(Structure):
|
|
205
|
+
_fields_ = [
|
|
206
|
+
("struct_size", c_uint32),
|
|
207
|
+
("gap_policy", c_uint32),
|
|
208
|
+
("since_seq", c_uint64),
|
|
209
|
+
("max_messages", c_uint64),
|
|
210
|
+
("timeout_ms", c_uint64),
|
|
211
|
+
("has_since", c_uint32),
|
|
212
|
+
("has_max", c_uint32),
|
|
213
|
+
("has_timeout", c_uint32),
|
|
214
|
+
("reserved", c_uint32),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _load_lib() -> CDLL:
|
|
219
|
+
env_dir = os.environ.get("PLASMITE_LIB_DIR")
|
|
220
|
+
if env_dir:
|
|
221
|
+
candidates = [
|
|
222
|
+
os.path.join(env_dir, name)
|
|
223
|
+
for name in ("plasmite.dll", "libplasmite.dylib", "libplasmite.so")
|
|
224
|
+
]
|
|
225
|
+
for candidate in candidates:
|
|
226
|
+
if os.path.exists(candidate):
|
|
227
|
+
return CDLL(candidate)
|
|
228
|
+
native_dir = Path(__file__).resolve().parent / "_native"
|
|
229
|
+
for candidate in (
|
|
230
|
+
native_dir / "plasmite.dll",
|
|
231
|
+
native_dir / "libplasmite.dylib",
|
|
232
|
+
native_dir / "libplasmite.so",
|
|
233
|
+
):
|
|
234
|
+
if candidate.exists():
|
|
235
|
+
return CDLL(str(candidate))
|
|
236
|
+
for name in ("plasmite.dll", "plasmite", "libplasmite"):
|
|
237
|
+
try:
|
|
238
|
+
return CDLL(name)
|
|
239
|
+
except OSError:
|
|
240
|
+
continue
|
|
241
|
+
raise OSError(
|
|
242
|
+
"libplasmite not found; set PLASMITE_LIB_DIR or build target/debug/libplasmite.*"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
_LIB = _load_lib()
|
|
247
|
+
|
|
248
|
+
DEFAULT_POOL_SIZE_BYTES = 1024 * 1024
|
|
249
|
+
DEFAULT_POOL_SIZE = DEFAULT_POOL_SIZE_BYTES
|
|
250
|
+
_UINT64_MAX = (1 << 64) - 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def default_pool_dir() -> str:
|
|
254
|
+
# Compute lazily so tests/tools can override HOME in-process without re-importing.
|
|
255
|
+
# `expanduser("~")` consults HOME at call time on Unix.
|
|
256
|
+
return os.path.join(os.path.expanduser("~"), ".plasmite", "pools")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
DEFAULT_POOL_DIR = default_pool_dir()
|
|
260
|
+
|
|
261
|
+
_LIB.plsm_client_new.argtypes = [c_char_p, POINTER(POINTER(plsm_client_t)), POINTER(POINTER(plsm_error_t))]
|
|
262
|
+
_LIB.plsm_client_new.restype = c_int
|
|
263
|
+
_LIB.plsm_client_free.argtypes = [POINTER(plsm_client_t)]
|
|
264
|
+
_LIB.plsm_client_free.restype = None
|
|
265
|
+
|
|
266
|
+
_LIB.plsm_pool_create.argtypes = [
|
|
267
|
+
POINTER(plsm_client_t),
|
|
268
|
+
c_char_p,
|
|
269
|
+
c_uint64,
|
|
270
|
+
POINTER(POINTER(plsm_pool_t)),
|
|
271
|
+
POINTER(POINTER(plsm_error_t)),
|
|
272
|
+
]
|
|
273
|
+
_LIB.plsm_pool_create.restype = c_int
|
|
274
|
+
_LIB.plsm_pool_open.argtypes = [
|
|
275
|
+
POINTER(plsm_client_t),
|
|
276
|
+
c_char_p,
|
|
277
|
+
POINTER(POINTER(plsm_pool_t)),
|
|
278
|
+
POINTER(POINTER(plsm_error_t)),
|
|
279
|
+
]
|
|
280
|
+
_LIB.plsm_pool_open.restype = c_int
|
|
281
|
+
_LIB.plsm_pool_free.argtypes = [POINTER(plsm_pool_t)]
|
|
282
|
+
_LIB.plsm_pool_free.restype = None
|
|
283
|
+
|
|
284
|
+
_LIB.plsm_pool_append_json.argtypes = [
|
|
285
|
+
POINTER(plsm_pool_t),
|
|
286
|
+
POINTER(c_uint8),
|
|
287
|
+
c_size_t,
|
|
288
|
+
POINTER(c_char_p),
|
|
289
|
+
c_size_t,
|
|
290
|
+
c_uint32,
|
|
291
|
+
POINTER(plsm_buf_t),
|
|
292
|
+
POINTER(POINTER(plsm_error_t)),
|
|
293
|
+
]
|
|
294
|
+
_LIB.plsm_pool_append_json.restype = c_int
|
|
295
|
+
|
|
296
|
+
_LIB.plsm_pool_append_lite3.argtypes = [
|
|
297
|
+
POINTER(plsm_pool_t),
|
|
298
|
+
POINTER(c_uint8),
|
|
299
|
+
c_size_t,
|
|
300
|
+
c_uint32,
|
|
301
|
+
POINTER(c_uint64),
|
|
302
|
+
POINTER(POINTER(plsm_error_t)),
|
|
303
|
+
]
|
|
304
|
+
_LIB.plsm_pool_append_lite3.restype = c_int
|
|
305
|
+
|
|
306
|
+
_LIB.plsm_pool_get_json.argtypes = [
|
|
307
|
+
POINTER(plsm_pool_t),
|
|
308
|
+
c_uint64,
|
|
309
|
+
POINTER(plsm_buf_t),
|
|
310
|
+
POINTER(POINTER(plsm_error_t)),
|
|
311
|
+
]
|
|
312
|
+
_LIB.plsm_pool_get_json.restype = c_int
|
|
313
|
+
|
|
314
|
+
_LIB.plsm_pool_get_lite3.argtypes = [
|
|
315
|
+
POINTER(plsm_pool_t),
|
|
316
|
+
c_uint64,
|
|
317
|
+
POINTER(plsm_lite3_frame_t),
|
|
318
|
+
POINTER(POINTER(plsm_error_t)),
|
|
319
|
+
]
|
|
320
|
+
_LIB.plsm_pool_get_lite3.restype = c_int
|
|
321
|
+
|
|
322
|
+
_LIB.plsm_stream_open.argtypes = [
|
|
323
|
+
POINTER(plsm_pool_t),
|
|
324
|
+
c_uint64,
|
|
325
|
+
c_uint32,
|
|
326
|
+
c_uint64,
|
|
327
|
+
c_uint32,
|
|
328
|
+
c_uint64,
|
|
329
|
+
c_uint32,
|
|
330
|
+
POINTER(POINTER(plsm_stream_t)),
|
|
331
|
+
POINTER(POINTER(plsm_error_t)),
|
|
332
|
+
]
|
|
333
|
+
_LIB.plsm_stream_open.restype = c_int
|
|
334
|
+
_LIB.plsm_stream_open_ex.argtypes = [
|
|
335
|
+
POINTER(plsm_pool_t),
|
|
336
|
+
POINTER(plsm_stream_options_t),
|
|
337
|
+
POINTER(POINTER(plsm_stream_t)),
|
|
338
|
+
POINTER(POINTER(plsm_error_t)),
|
|
339
|
+
]
|
|
340
|
+
_LIB.plsm_stream_open_ex.restype = c_int
|
|
341
|
+
|
|
342
|
+
_LIB.plsm_lite3_stream_open.argtypes = [
|
|
343
|
+
POINTER(plsm_pool_t),
|
|
344
|
+
c_uint64,
|
|
345
|
+
c_uint32,
|
|
346
|
+
c_uint64,
|
|
347
|
+
c_uint32,
|
|
348
|
+
c_uint64,
|
|
349
|
+
c_uint32,
|
|
350
|
+
POINTER(POINTER(plsm_lite3_stream_t)),
|
|
351
|
+
POINTER(POINTER(plsm_error_t)),
|
|
352
|
+
]
|
|
353
|
+
_LIB.plsm_lite3_stream_open.restype = c_int
|
|
354
|
+
_LIB.plsm_lite3_stream_open_ex.argtypes = [
|
|
355
|
+
POINTER(plsm_pool_t),
|
|
356
|
+
POINTER(plsm_stream_options_t),
|
|
357
|
+
POINTER(POINTER(plsm_lite3_stream_t)),
|
|
358
|
+
POINTER(POINTER(plsm_error_t)),
|
|
359
|
+
]
|
|
360
|
+
_LIB.plsm_lite3_stream_open_ex.restype = c_int
|
|
361
|
+
|
|
362
|
+
_LIB.plsm_stream_next.argtypes = [
|
|
363
|
+
POINTER(plsm_stream_t),
|
|
364
|
+
POINTER(plsm_buf_t),
|
|
365
|
+
POINTER(POINTER(plsm_error_t)),
|
|
366
|
+
]
|
|
367
|
+
_LIB.plsm_stream_next.restype = c_int
|
|
368
|
+
|
|
369
|
+
_LIB.plsm_lite3_stream_next.argtypes = [
|
|
370
|
+
POINTER(plsm_lite3_stream_t),
|
|
371
|
+
POINTER(plsm_lite3_frame_t),
|
|
372
|
+
POINTER(POINTER(plsm_error_t)),
|
|
373
|
+
]
|
|
374
|
+
_LIB.plsm_lite3_stream_next.restype = c_int
|
|
375
|
+
|
|
376
|
+
_LIB.plsm_stream_free.argtypes = [POINTER(plsm_stream_t)]
|
|
377
|
+
_LIB.plsm_stream_free.restype = None
|
|
378
|
+
|
|
379
|
+
_LIB.plsm_lite3_stream_free.argtypes = [POINTER(plsm_lite3_stream_t)]
|
|
380
|
+
_LIB.plsm_lite3_stream_free.restype = None
|
|
381
|
+
|
|
382
|
+
_LIB.plsm_buf_free.argtypes = [POINTER(plsm_buf_t)]
|
|
383
|
+
_LIB.plsm_buf_free.restype = None
|
|
384
|
+
|
|
385
|
+
_LIB.plsm_lite3_frame_free.argtypes = [POINTER(plsm_lite3_frame_t)]
|
|
386
|
+
_LIB.plsm_lite3_frame_free.restype = None
|
|
387
|
+
|
|
388
|
+
_LIB.plsm_error_free.argtypes = [POINTER(plsm_error_t)]
|
|
389
|
+
_LIB.plsm_error_free.restype = None
|
|
390
|
+
|
|
391
|
+
_LIB.plsm_udp_client_new.argtypes = [
|
|
392
|
+
c_char_p,
|
|
393
|
+
POINTER(POINTER(plsm_udp_client_t)),
|
|
394
|
+
POINTER(POINTER(plsm_error_t)),
|
|
395
|
+
]
|
|
396
|
+
_LIB.plsm_udp_client_new.restype = c_int
|
|
397
|
+
_LIB.plsm_udp_client_free.argtypes = [POINTER(plsm_udp_client_t)]
|
|
398
|
+
_LIB.plsm_udp_client_free.restype = None
|
|
399
|
+
_LIB.plsm_udp_client_open_pool.argtypes = [
|
|
400
|
+
POINTER(plsm_udp_client_t),
|
|
401
|
+
c_char_p,
|
|
402
|
+
POINTER(POINTER(plsm_udp_pool_t)),
|
|
403
|
+
POINTER(POINTER(plsm_error_t)),
|
|
404
|
+
]
|
|
405
|
+
_LIB.plsm_udp_client_open_pool.restype = c_int
|
|
406
|
+
_LIB.plsm_udp_pool_free.argtypes = [POINTER(plsm_udp_pool_t)]
|
|
407
|
+
_LIB.plsm_udp_pool_free.restype = None
|
|
408
|
+
_LIB.plsm_udp_pool_send_json.argtypes = [
|
|
409
|
+
POINTER(plsm_udp_pool_t),
|
|
410
|
+
POINTER(c_uint8),
|
|
411
|
+
c_size_t,
|
|
412
|
+
POINTER(c_char_p),
|
|
413
|
+
c_size_t,
|
|
414
|
+
POINTER(POINTER(plsm_error_t)),
|
|
415
|
+
]
|
|
416
|
+
_LIB.plsm_udp_pool_send_json.restype = c_int
|
|
417
|
+
_LIB.plsm_udp_pool_send_lite3.argtypes = [
|
|
418
|
+
POINTER(plsm_udp_pool_t),
|
|
419
|
+
POINTER(c_uint8),
|
|
420
|
+
c_size_t,
|
|
421
|
+
POINTER(POINTER(plsm_error_t)),
|
|
422
|
+
]
|
|
423
|
+
_LIB.plsm_udp_pool_send_lite3.restype = c_int
|
|
424
|
+
_LIB.plsm_udp_pool_follow.argtypes = [
|
|
425
|
+
POINTER(plsm_udp_pool_t),
|
|
426
|
+
c_uint32,
|
|
427
|
+
c_uint64,
|
|
428
|
+
c_uint32,
|
|
429
|
+
POINTER(POINTER(plsm_udp_follow_t)),
|
|
430
|
+
POINTER(POINTER(plsm_error_t)),
|
|
431
|
+
]
|
|
432
|
+
_LIB.plsm_udp_pool_follow.restype = c_int
|
|
433
|
+
_LIB.plsm_udp_follow_next.argtypes = [
|
|
434
|
+
POINTER(plsm_udp_follow_t),
|
|
435
|
+
c_uint64,
|
|
436
|
+
POINTER(plsm_udp_event_t),
|
|
437
|
+
POINTER(POINTER(plsm_error_t)),
|
|
438
|
+
]
|
|
439
|
+
_LIB.plsm_udp_follow_next.restype = c_int
|
|
440
|
+
_LIB.plsm_udp_follow_cancel.argtypes = [POINTER(plsm_udp_follow_t)]
|
|
441
|
+
_LIB.plsm_udp_follow_cancel.restype = None
|
|
442
|
+
_LIB.plsm_udp_follow_close.argtypes = [POINTER(plsm_udp_follow_t)]
|
|
443
|
+
_LIB.plsm_udp_follow_close.restype = None
|
|
444
|
+
_LIB.plsm_udp_event_free.argtypes = [POINTER(plsm_udp_event_t)]
|
|
445
|
+
_LIB.plsm_udp_event_free.restype = None
|
|
446
|
+
_LIB.plsm_udp_event_message.argtypes = [
|
|
447
|
+
POINTER(plsm_udp_event_t),
|
|
448
|
+
POINTER(plsm_buf_t),
|
|
449
|
+
POINTER(POINTER(plsm_error_t)),
|
|
450
|
+
]
|
|
451
|
+
_LIB.plsm_udp_event_message.restype = c_int
|
|
452
|
+
|
|
453
|
+
_ERROR_KIND_TO_CLASS: dict[ErrorKind, type[PlasmiteError]] = {
|
|
454
|
+
ErrorKind.INTERNAL: InternalError,
|
|
455
|
+
ErrorKind.USAGE: UsageError,
|
|
456
|
+
ErrorKind.NOT_FOUND: NotFoundError,
|
|
457
|
+
ErrorKind.ALREADY_EXISTS: AlreadyExistsError,
|
|
458
|
+
ErrorKind.BUSY: BusyError,
|
|
459
|
+
ErrorKind.PERMISSION: PermissionDeniedError,
|
|
460
|
+
ErrorKind.CORRUPT: CorruptError,
|
|
461
|
+
ErrorKind.IO: IoError,
|
|
462
|
+
ErrorKind.RETENTION_GAP: RetentionGapError,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _take_error(err_ptr: POINTER(plsm_error_t) | None) -> PlasmiteError:
|
|
467
|
+
if not err_ptr:
|
|
468
|
+
return PlasmiteError(ErrorKind.INTERNAL, "plasmite: unknown error")
|
|
469
|
+
err = err_ptr.contents
|
|
470
|
+
raw_kind = int(err.kind)
|
|
471
|
+
message = err.message.decode("utf-8") if err.message else ""
|
|
472
|
+
path = err.path.decode("utf-8") if err.path else None
|
|
473
|
+
seq = int(err.seq) if err.has_seq else None
|
|
474
|
+
offset = int(err.offset) if err.has_offset else None
|
|
475
|
+
_LIB.plsm_error_free(err_ptr)
|
|
476
|
+
try:
|
|
477
|
+
kind = ErrorKind(raw_kind)
|
|
478
|
+
except ValueError:
|
|
479
|
+
kind = ErrorKind.INTERNAL
|
|
480
|
+
if not message:
|
|
481
|
+
message = _default_error_message(kind)
|
|
482
|
+
error_cls = _ERROR_KIND_TO_CLASS.get(kind, PlasmiteError)
|
|
483
|
+
return error_cls(kind, message, path, seq, offset)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _default_error_message(kind: ErrorKind) -> str:
|
|
487
|
+
mapping = {
|
|
488
|
+
ErrorKind.INTERNAL: "internal error",
|
|
489
|
+
ErrorKind.USAGE: "usage error",
|
|
490
|
+
ErrorKind.NOT_FOUND: "not found",
|
|
491
|
+
ErrorKind.ALREADY_EXISTS: "already exists",
|
|
492
|
+
ErrorKind.BUSY: "busy",
|
|
493
|
+
ErrorKind.PERMISSION: "permission denied",
|
|
494
|
+
ErrorKind.CORRUPT: "corrupt",
|
|
495
|
+
ErrorKind.IO: "io error",
|
|
496
|
+
ErrorKind.RETENTION_GAP: "retention gap",
|
|
497
|
+
}
|
|
498
|
+
return mapping.get(kind, "error")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _buf_to_bytes(buf: plsm_buf_t) -> bytes:
|
|
502
|
+
if not buf.data or buf.len == 0:
|
|
503
|
+
_LIB.plsm_buf_free(byref(buf))
|
|
504
|
+
return b""
|
|
505
|
+
data = (c_uint8 * buf.len).from_address(buf.data)
|
|
506
|
+
out = bytes(data)
|
|
507
|
+
_LIB.plsm_buf_free(byref(buf))
|
|
508
|
+
return out
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _frame_to_py(frame: plsm_lite3_frame_t) -> Lite3Frame:
|
|
512
|
+
seq = int(frame.seq)
|
|
513
|
+
timestamp_ns = int(frame.timestamp_ns)
|
|
514
|
+
flags = int(frame.flags)
|
|
515
|
+
payload = b""
|
|
516
|
+
if frame.payload.data and frame.payload.len:
|
|
517
|
+
data = (c_uint8 * frame.payload.len).from_address(frame.payload.data)
|
|
518
|
+
payload = bytes(data)
|
|
519
|
+
_LIB.plsm_lite3_frame_free(byref(frame))
|
|
520
|
+
return Lite3Frame(seq=seq, timestamp_ns=timestamp_ns, flags=flags, payload=payload)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _tag_array(values: Iterable[str]) -> tuple[POINTER(c_char_p), list[bytes]]:
|
|
524
|
+
encoded = [value.encode("utf-8") for value in values]
|
|
525
|
+
if not encoded:
|
|
526
|
+
return POINTER(c_char_p)(), encoded
|
|
527
|
+
arr = (c_char_p * len(encoded))()
|
|
528
|
+
for idx, val in enumerate(encoded):
|
|
529
|
+
arr[idx] = val
|
|
530
|
+
return arr, encoded
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _closed_error(target: str) -> PlasmiteError:
|
|
534
|
+
return PlasmiteError(ErrorKind.USAGE, f"plasmite: {target} is closed")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _require_open(ptr: object, target: str) -> None:
|
|
538
|
+
if not ptr:
|
|
539
|
+
raise _closed_error(target)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _serialized_handle(method):
|
|
543
|
+
"""Keep handle checks, native calls, and owned-result cleanup together."""
|
|
544
|
+
@wraps(method)
|
|
545
|
+
def locked(self, *args, **kwargs):
|
|
546
|
+
with self._lock:
|
|
547
|
+
return method(self, *args, **kwargs)
|
|
548
|
+
|
|
549
|
+
return locked
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _ensure_bytes(payload: bytes | bytearray | memoryview, name: str) -> bytes:
|
|
553
|
+
if isinstance(payload, bytes):
|
|
554
|
+
return payload
|
|
555
|
+
if isinstance(payload, (bytearray, memoryview)):
|
|
556
|
+
return bytes(payload)
|
|
557
|
+
raise TypeError(f"{name} must be bytes-like")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _ensure_non_negative_int(value: int, name: str) -> int:
|
|
561
|
+
if not isinstance(value, int):
|
|
562
|
+
raise TypeError(f"{name} must be an int")
|
|
563
|
+
if value < 0:
|
|
564
|
+
raise ValueError(f"{name} must be >= 0")
|
|
565
|
+
if value > _UINT64_MAX:
|
|
566
|
+
raise ValueError(f"{name} must fit an unsigned 64-bit integer")
|
|
567
|
+
return value
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _optional_non_negative_int(value: Optional[int], name: str) -> Optional[int]:
|
|
571
|
+
if value is None:
|
|
572
|
+
return None
|
|
573
|
+
return _ensure_non_negative_int(value, name)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _stream_options(
|
|
577
|
+
since_seq: Optional[int],
|
|
578
|
+
max_messages: Optional[int],
|
|
579
|
+
timeout_ms: Optional[int],
|
|
580
|
+
error_on_gap: bool,
|
|
581
|
+
) -> plsm_stream_options_t:
|
|
582
|
+
options = plsm_stream_options_t()
|
|
583
|
+
options.struct_size = sizeof(plsm_stream_options_t)
|
|
584
|
+
options.gap_policy = 1 if error_on_gap else 0
|
|
585
|
+
options.since_seq = since_seq or 0
|
|
586
|
+
options.max_messages = max_messages or 0
|
|
587
|
+
options.timeout_ms = timeout_ms or 0
|
|
588
|
+
options.has_since = 1 if since_seq is not None else 0
|
|
589
|
+
options.has_max = 1 if max_messages is not None else 0
|
|
590
|
+
options.has_timeout = 1 if timeout_ms is not None else 0
|
|
591
|
+
return options
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class Client:
|
|
595
|
+
def __init__(self, pool_dir: str | None = None) -> None:
|
|
596
|
+
self._lock = RLock()
|
|
597
|
+
self._ptr = None
|
|
598
|
+
if pool_dir is None:
|
|
599
|
+
pool_dir = default_pool_dir()
|
|
600
|
+
if not pool_dir:
|
|
601
|
+
raise ValueError("pool_dir is required")
|
|
602
|
+
out_client = POINTER(plsm_client_t)()
|
|
603
|
+
out_err = POINTER(plsm_error_t)()
|
|
604
|
+
rc = _LIB.plsm_client_new(pool_dir.encode("utf-8"), byref(out_client), byref(out_err))
|
|
605
|
+
if rc != 0:
|
|
606
|
+
raise _take_error(out_err)
|
|
607
|
+
self._ptr = out_client
|
|
608
|
+
|
|
609
|
+
@_serialized_handle
|
|
610
|
+
def create_pool(
|
|
611
|
+
self,
|
|
612
|
+
pool_ref: str,
|
|
613
|
+
size_bytes: int = DEFAULT_POOL_SIZE_BYTES,
|
|
614
|
+
) -> Pool:
|
|
615
|
+
_require_open(self._ptr, "client")
|
|
616
|
+
if not pool_ref:
|
|
617
|
+
raise ValueError("pool_ref is required")
|
|
618
|
+
size_bytes = _ensure_non_negative_int(size_bytes, "size_bytes")
|
|
619
|
+
out_pool = POINTER(plsm_pool_t)()
|
|
620
|
+
out_err = POINTER(plsm_error_t)()
|
|
621
|
+
rc = _LIB.plsm_pool_create(
|
|
622
|
+
self._ptr,
|
|
623
|
+
pool_ref.encode("utf-8"),
|
|
624
|
+
c_uint64(size_bytes),
|
|
625
|
+
byref(out_pool),
|
|
626
|
+
byref(out_err),
|
|
627
|
+
)
|
|
628
|
+
if rc != 0:
|
|
629
|
+
raise _take_error(out_err)
|
|
630
|
+
return Pool(out_pool)
|
|
631
|
+
|
|
632
|
+
@_serialized_handle
|
|
633
|
+
def open_pool(self, pool_ref: str) -> Pool:
|
|
634
|
+
_require_open(self._ptr, "client")
|
|
635
|
+
if not pool_ref:
|
|
636
|
+
raise ValueError("pool_ref is required")
|
|
637
|
+
out_pool = POINTER(plsm_pool_t)()
|
|
638
|
+
out_err = POINTER(plsm_error_t)()
|
|
639
|
+
rc = _LIB.plsm_pool_open(
|
|
640
|
+
self._ptr,
|
|
641
|
+
pool_ref.encode("utf-8"),
|
|
642
|
+
byref(out_pool),
|
|
643
|
+
byref(out_err),
|
|
644
|
+
)
|
|
645
|
+
if rc != 0:
|
|
646
|
+
raise _take_error(out_err)
|
|
647
|
+
return Pool(out_pool)
|
|
648
|
+
|
|
649
|
+
def pool(
|
|
650
|
+
self,
|
|
651
|
+
pool_ref: str,
|
|
652
|
+
size_bytes: int = DEFAULT_POOL_SIZE_BYTES,
|
|
653
|
+
) -> Pool:
|
|
654
|
+
try:
|
|
655
|
+
return self.open_pool(pool_ref)
|
|
656
|
+
except NotFoundError:
|
|
657
|
+
return self.create_pool(pool_ref, size_bytes)
|
|
658
|
+
|
|
659
|
+
@_serialized_handle
|
|
660
|
+
def close(self) -> None:
|
|
661
|
+
if getattr(self, "_ptr", None):
|
|
662
|
+
_LIB.plsm_client_free(self._ptr)
|
|
663
|
+
self._ptr = None
|
|
664
|
+
|
|
665
|
+
def __enter__(self) -> Client:
|
|
666
|
+
with self._lock:
|
|
667
|
+
_require_open(self._ptr, "client")
|
|
668
|
+
return self
|
|
669
|
+
|
|
670
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
671
|
+
self.close()
|
|
672
|
+
|
|
673
|
+
def __del__(self) -> None:
|
|
674
|
+
self.close()
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@dataclass(frozen=True)
|
|
678
|
+
class Lite3Frame:
|
|
679
|
+
seq: int
|
|
680
|
+
timestamp_ns: int
|
|
681
|
+
flags: int
|
|
682
|
+
payload: bytes
|
|
683
|
+
|
|
684
|
+
@property
|
|
685
|
+
def time(self) -> datetime:
|
|
686
|
+
seconds, nanoseconds = divmod(self.timestamp_ns, 1_000_000_000)
|
|
687
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(
|
|
688
|
+
microsecond=nanoseconds // 1_000
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class Pool:
|
|
693
|
+
def __init__(self, ptr: POINTER(plsm_pool_t)) -> None:
|
|
694
|
+
self._lock = RLock()
|
|
695
|
+
self._ptr = ptr
|
|
696
|
+
|
|
697
|
+
@_serialized_handle
|
|
698
|
+
def append_json(self, payload: bytes, tags: Iterable[str], durability: Durability) -> bytes:
|
|
699
|
+
_require_open(self._ptr, "pool")
|
|
700
|
+
payload = _ensure_bytes(payload, "payload")
|
|
701
|
+
if not payload:
|
|
702
|
+
raise ValueError("payload is required")
|
|
703
|
+
buf = plsm_buf_t()
|
|
704
|
+
out_err = POINTER(plsm_error_t)()
|
|
705
|
+
arr, _keep = _tag_array(tags)
|
|
706
|
+
payload_buf = (c_uint8 * len(payload)).from_buffer_copy(payload)
|
|
707
|
+
rc = _LIB.plsm_pool_append_json(
|
|
708
|
+
self._ptr,
|
|
709
|
+
payload_buf,
|
|
710
|
+
c_size_t(len(payload)),
|
|
711
|
+
arr,
|
|
712
|
+
c_size_t(len(_keep)),
|
|
713
|
+
c_uint32(int(durability)),
|
|
714
|
+
byref(buf),
|
|
715
|
+
byref(out_err),
|
|
716
|
+
)
|
|
717
|
+
if rc != 0:
|
|
718
|
+
raise _take_error(out_err)
|
|
719
|
+
return _buf_to_bytes(buf)
|
|
720
|
+
|
|
721
|
+
def append(
|
|
722
|
+
self,
|
|
723
|
+
value,
|
|
724
|
+
tags: Optional[Iterable[str]] = None,
|
|
725
|
+
durability: Durability = Durability.FAST,
|
|
726
|
+
) -> Message:
|
|
727
|
+
payload = self.append_json(
|
|
728
|
+
json.dumps(value).encode("utf-8"),
|
|
729
|
+
[] if tags is None else tags,
|
|
730
|
+
durability,
|
|
731
|
+
)
|
|
732
|
+
return parse_message(payload)
|
|
733
|
+
|
|
734
|
+
@_serialized_handle
|
|
735
|
+
def append_lite3(self, payload: bytes, durability: Durability) -> int:
|
|
736
|
+
_require_open(self._ptr, "pool")
|
|
737
|
+
payload = _ensure_bytes(payload, "payload")
|
|
738
|
+
if not payload:
|
|
739
|
+
raise ValueError("payload is required")
|
|
740
|
+
out_seq = c_uint64()
|
|
741
|
+
out_err = POINTER(plsm_error_t)()
|
|
742
|
+
payload_buf = (c_uint8 * len(payload)).from_buffer_copy(payload)
|
|
743
|
+
rc = _LIB.plsm_pool_append_lite3(
|
|
744
|
+
self._ptr,
|
|
745
|
+
payload_buf,
|
|
746
|
+
c_size_t(len(payload)),
|
|
747
|
+
c_uint32(int(durability)),
|
|
748
|
+
byref(out_seq),
|
|
749
|
+
byref(out_err),
|
|
750
|
+
)
|
|
751
|
+
if rc != 0:
|
|
752
|
+
raise _take_error(out_err)
|
|
753
|
+
return int(out_seq.value)
|
|
754
|
+
|
|
755
|
+
@_serialized_handle
|
|
756
|
+
def get_json(self, seq: int) -> bytes:
|
|
757
|
+
_require_open(self._ptr, "pool")
|
|
758
|
+
seq = _ensure_non_negative_int(seq, "seq")
|
|
759
|
+
buf = plsm_buf_t()
|
|
760
|
+
out_err = POINTER(plsm_error_t)()
|
|
761
|
+
rc = _LIB.plsm_pool_get_json(self._ptr, c_uint64(seq), byref(buf), byref(out_err))
|
|
762
|
+
if rc != 0:
|
|
763
|
+
raise _take_error(out_err)
|
|
764
|
+
return _buf_to_bytes(buf)
|
|
765
|
+
|
|
766
|
+
def get(self, seq: int) -> Message:
|
|
767
|
+
return parse_message(self.get_json(seq))
|
|
768
|
+
|
|
769
|
+
@_serialized_handle
|
|
770
|
+
def get_lite3(self, seq: int) -> Lite3Frame:
|
|
771
|
+
_require_open(self._ptr, "pool")
|
|
772
|
+
seq = _ensure_non_negative_int(seq, "seq")
|
|
773
|
+
frame = plsm_lite3_frame_t()
|
|
774
|
+
out_err = POINTER(plsm_error_t)()
|
|
775
|
+
rc = _LIB.plsm_pool_get_lite3(self._ptr, c_uint64(seq), byref(frame), byref(out_err))
|
|
776
|
+
if rc != 0:
|
|
777
|
+
raise _take_error(out_err)
|
|
778
|
+
return _frame_to_py(frame)
|
|
779
|
+
|
|
780
|
+
@_serialized_handle
|
|
781
|
+
def open_stream(
|
|
782
|
+
self,
|
|
783
|
+
since_seq: Optional[int] = None,
|
|
784
|
+
max_messages: Optional[int] = None,
|
|
785
|
+
timeout_ms: Optional[int] = None,
|
|
786
|
+
error_on_gap: bool = False,
|
|
787
|
+
) -> Stream:
|
|
788
|
+
_require_open(self._ptr, "pool")
|
|
789
|
+
since_seq = _optional_non_negative_int(since_seq, "since_seq")
|
|
790
|
+
max_messages = _optional_non_negative_int(max_messages, "max_messages")
|
|
791
|
+
timeout_ms = _optional_non_negative_int(timeout_ms, "timeout_ms")
|
|
792
|
+
out_stream = POINTER(plsm_stream_t)()
|
|
793
|
+
out_err = POINTER(plsm_error_t)()
|
|
794
|
+
if error_on_gap:
|
|
795
|
+
options = _stream_options(since_seq, max_messages, timeout_ms, True)
|
|
796
|
+
rc = _LIB.plsm_stream_open_ex(
|
|
797
|
+
self._ptr,
|
|
798
|
+
byref(options),
|
|
799
|
+
byref(out_stream),
|
|
800
|
+
byref(out_err),
|
|
801
|
+
)
|
|
802
|
+
else:
|
|
803
|
+
rc = _LIB.plsm_stream_open(
|
|
804
|
+
self._ptr,
|
|
805
|
+
c_uint64(since_seq or 0),
|
|
806
|
+
c_uint32(1 if since_seq is not None else 0),
|
|
807
|
+
c_uint64(max_messages or 0),
|
|
808
|
+
c_uint32(1 if max_messages is not None else 0),
|
|
809
|
+
c_uint64(timeout_ms or 0),
|
|
810
|
+
c_uint32(1 if timeout_ms is not None else 0),
|
|
811
|
+
byref(out_stream),
|
|
812
|
+
byref(out_err),
|
|
813
|
+
)
|
|
814
|
+
if rc != 0:
|
|
815
|
+
raise _take_error(out_err)
|
|
816
|
+
return Stream(out_stream)
|
|
817
|
+
|
|
818
|
+
@_serialized_handle
|
|
819
|
+
def open_lite3_stream(
|
|
820
|
+
self,
|
|
821
|
+
since_seq: Optional[int] = None,
|
|
822
|
+
max_messages: Optional[int] = None,
|
|
823
|
+
timeout_ms: Optional[int] = None,
|
|
824
|
+
error_on_gap: bool = False,
|
|
825
|
+
) -> Lite3Stream:
|
|
826
|
+
_require_open(self._ptr, "pool")
|
|
827
|
+
since_seq = _optional_non_negative_int(since_seq, "since_seq")
|
|
828
|
+
max_messages = _optional_non_negative_int(max_messages, "max_messages")
|
|
829
|
+
timeout_ms = _optional_non_negative_int(timeout_ms, "timeout_ms")
|
|
830
|
+
out_stream = POINTER(plsm_lite3_stream_t)()
|
|
831
|
+
out_err = POINTER(plsm_error_t)()
|
|
832
|
+
if error_on_gap:
|
|
833
|
+
options = _stream_options(since_seq, max_messages, timeout_ms, True)
|
|
834
|
+
rc = _LIB.plsm_lite3_stream_open_ex(
|
|
835
|
+
self._ptr,
|
|
836
|
+
byref(options),
|
|
837
|
+
byref(out_stream),
|
|
838
|
+
byref(out_err),
|
|
839
|
+
)
|
|
840
|
+
else:
|
|
841
|
+
rc = _LIB.plsm_lite3_stream_open(
|
|
842
|
+
self._ptr,
|
|
843
|
+
c_uint64(since_seq or 0),
|
|
844
|
+
c_uint32(1 if since_seq is not None else 0),
|
|
845
|
+
c_uint64(max_messages or 0),
|
|
846
|
+
c_uint32(1 if max_messages is not None else 0),
|
|
847
|
+
c_uint64(timeout_ms or 0),
|
|
848
|
+
c_uint32(1 if timeout_ms is not None else 0),
|
|
849
|
+
byref(out_stream),
|
|
850
|
+
byref(out_err),
|
|
851
|
+
)
|
|
852
|
+
if rc != 0:
|
|
853
|
+
raise _take_error(out_err)
|
|
854
|
+
return Lite3Stream(out_stream)
|
|
855
|
+
|
|
856
|
+
def replay(
|
|
857
|
+
self,
|
|
858
|
+
speed: float = 1.0,
|
|
859
|
+
since_seq: Optional[int] = None,
|
|
860
|
+
max_messages: Optional[int] = None,
|
|
861
|
+
timeout_ms: Optional[int] = None,
|
|
862
|
+
tags: Optional[Iterable[str]] = None,
|
|
863
|
+
) -> Generator[Message, None, None]:
|
|
864
|
+
"""Replay messages with original timing scaled by speed.
|
|
865
|
+
|
|
866
|
+
Yields Message objects with inter-message delays derived from
|
|
867
|
+
each message's ``time``. The first message is yielded
|
|
868
|
+
immediately; subsequent messages are delayed by
|
|
869
|
+
``(current_time - prev_time) / speed``.
|
|
870
|
+
"""
|
|
871
|
+
if speed <= 0:
|
|
872
|
+
raise ValueError("speed must be positive")
|
|
873
|
+
|
|
874
|
+
required_tags = list(tags or [])
|
|
875
|
+
stream_max_messages = (
|
|
876
|
+
None if (max_messages is not None and required_tags) else max_messages
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
stream = self.open_stream(
|
|
880
|
+
since_seq=since_seq,
|
|
881
|
+
max_messages=stream_max_messages,
|
|
882
|
+
timeout_ms=timeout_ms,
|
|
883
|
+
)
|
|
884
|
+
try:
|
|
885
|
+
prev_dt: Optional[datetime] = None
|
|
886
|
+
delivered = 0
|
|
887
|
+
filter_by_tags = bool(required_tags)
|
|
888
|
+
while True:
|
|
889
|
+
msg = stream.next_json()
|
|
890
|
+
if msg is None:
|
|
891
|
+
break
|
|
892
|
+
message = parse_message(msg)
|
|
893
|
+
if filter_by_tags and not _message_has_tags(message, required_tags):
|
|
894
|
+
continue
|
|
895
|
+
cur_dt = message.time
|
|
896
|
+
if prev_dt is not None and cur_dt is not None:
|
|
897
|
+
delta = (cur_dt - prev_dt).total_seconds() / speed
|
|
898
|
+
if delta > 0:
|
|
899
|
+
_time.sleep(delta)
|
|
900
|
+
if cur_dt is not None:
|
|
901
|
+
prev_dt = cur_dt
|
|
902
|
+
delivered += 1
|
|
903
|
+
yield message
|
|
904
|
+
if max_messages is not None and delivered >= max_messages:
|
|
905
|
+
break
|
|
906
|
+
finally:
|
|
907
|
+
stream.close()
|
|
908
|
+
|
|
909
|
+
def tail(
|
|
910
|
+
self,
|
|
911
|
+
since_seq: Optional[int] = None,
|
|
912
|
+
max_messages: Optional[int] = None,
|
|
913
|
+
timeout_ms: Optional[int] = None,
|
|
914
|
+
tags: Optional[Iterable[str]] = None,
|
|
915
|
+
error_on_gap: bool = False,
|
|
916
|
+
) -> Generator[Message, None, None]:
|
|
917
|
+
"""Tail JSON messages and optionally filter by exact tags."""
|
|
918
|
+
required_tags = list(tags or [])
|
|
919
|
+
stream_max_messages = (
|
|
920
|
+
None if (max_messages is not None and required_tags) else max_messages
|
|
921
|
+
)
|
|
922
|
+
stream = self.open_stream(
|
|
923
|
+
since_seq=since_seq,
|
|
924
|
+
max_messages=stream_max_messages,
|
|
925
|
+
timeout_ms=timeout_ms,
|
|
926
|
+
error_on_gap=error_on_gap,
|
|
927
|
+
)
|
|
928
|
+
try:
|
|
929
|
+
delivered = 0
|
|
930
|
+
filter_by_tags = bool(required_tags)
|
|
931
|
+
while True:
|
|
932
|
+
msg = stream.next_json()
|
|
933
|
+
if msg is None:
|
|
934
|
+
break
|
|
935
|
+
message = parse_message(msg)
|
|
936
|
+
if filter_by_tags:
|
|
937
|
+
if not _message_has_tags(message, required_tags):
|
|
938
|
+
continue
|
|
939
|
+
delivered += 1
|
|
940
|
+
yield message
|
|
941
|
+
if max_messages is not None and delivered >= max_messages:
|
|
942
|
+
break
|
|
943
|
+
finally:
|
|
944
|
+
stream.close()
|
|
945
|
+
|
|
946
|
+
@_serialized_handle
|
|
947
|
+
def close(self) -> None:
|
|
948
|
+
if getattr(self, "_ptr", None):
|
|
949
|
+
_LIB.plsm_pool_free(self._ptr)
|
|
950
|
+
self._ptr = None
|
|
951
|
+
|
|
952
|
+
def __enter__(self) -> Pool:
|
|
953
|
+
with self._lock:
|
|
954
|
+
_require_open(self._ptr, "pool")
|
|
955
|
+
return self
|
|
956
|
+
|
|
957
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
958
|
+
self.close()
|
|
959
|
+
|
|
960
|
+
def __del__(self) -> None:
|
|
961
|
+
self.close()
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class Stream:
|
|
965
|
+
def __init__(self, ptr: POINTER(plsm_stream_t)) -> None:
|
|
966
|
+
self._lock = RLock()
|
|
967
|
+
self._ptr = ptr
|
|
968
|
+
|
|
969
|
+
@_serialized_handle
|
|
970
|
+
def next_json(self) -> Optional[bytes]:
|
|
971
|
+
_require_open(self._ptr, "stream")
|
|
972
|
+
buf = plsm_buf_t()
|
|
973
|
+
out_err = POINTER(plsm_error_t)()
|
|
974
|
+
rc = _LIB.plsm_stream_next(self._ptr, byref(buf), byref(out_err))
|
|
975
|
+
if rc == 1:
|
|
976
|
+
return _buf_to_bytes(buf)
|
|
977
|
+
if rc == 0:
|
|
978
|
+
return None
|
|
979
|
+
raise _take_error(out_err)
|
|
980
|
+
|
|
981
|
+
def __iter__(self) -> Stream:
|
|
982
|
+
return self
|
|
983
|
+
|
|
984
|
+
def __next__(self) -> bytes:
|
|
985
|
+
message = self.next_json()
|
|
986
|
+
if message is None:
|
|
987
|
+
raise StopIteration
|
|
988
|
+
return message
|
|
989
|
+
|
|
990
|
+
@_serialized_handle
|
|
991
|
+
def close(self) -> None:
|
|
992
|
+
if getattr(self, "_ptr", None):
|
|
993
|
+
_LIB.plsm_stream_free(self._ptr)
|
|
994
|
+
self._ptr = None
|
|
995
|
+
|
|
996
|
+
def __enter__(self) -> Stream:
|
|
997
|
+
with self._lock:
|
|
998
|
+
_require_open(self._ptr, "stream")
|
|
999
|
+
return self
|
|
1000
|
+
|
|
1001
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
1002
|
+
self.close()
|
|
1003
|
+
|
|
1004
|
+
def __del__(self) -> None:
|
|
1005
|
+
self.close()
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
class Lite3Stream:
|
|
1009
|
+
def __init__(self, ptr: POINTER(plsm_lite3_stream_t)) -> None:
|
|
1010
|
+
self._lock = RLock()
|
|
1011
|
+
self._ptr = ptr
|
|
1012
|
+
|
|
1013
|
+
@_serialized_handle
|
|
1014
|
+
def next(self) -> Optional[Lite3Frame]:
|
|
1015
|
+
_require_open(self._ptr, "stream")
|
|
1016
|
+
frame = plsm_lite3_frame_t()
|
|
1017
|
+
out_err = POINTER(plsm_error_t)()
|
|
1018
|
+
rc = _LIB.plsm_lite3_stream_next(self._ptr, byref(frame), byref(out_err))
|
|
1019
|
+
if rc == 1:
|
|
1020
|
+
return _frame_to_py(frame)
|
|
1021
|
+
if rc == 0:
|
|
1022
|
+
return None
|
|
1023
|
+
raise _take_error(out_err)
|
|
1024
|
+
|
|
1025
|
+
def __iter__(self) -> Lite3Stream:
|
|
1026
|
+
return self
|
|
1027
|
+
|
|
1028
|
+
def __next__(self) -> Lite3Frame:
|
|
1029
|
+
frame = self.next()
|
|
1030
|
+
if frame is None:
|
|
1031
|
+
raise StopIteration
|
|
1032
|
+
return frame
|
|
1033
|
+
|
|
1034
|
+
@_serialized_handle
|
|
1035
|
+
def close(self) -> None:
|
|
1036
|
+
if getattr(self, "_ptr", None):
|
|
1037
|
+
_LIB.plsm_lite3_stream_free(self._ptr)
|
|
1038
|
+
self._ptr = None
|
|
1039
|
+
|
|
1040
|
+
def __enter__(self) -> Lite3Stream:
|
|
1041
|
+
with self._lock:
|
|
1042
|
+
_require_open(self._ptr, "stream")
|
|
1043
|
+
return self
|
|
1044
|
+
|
|
1045
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
1046
|
+
self.close()
|
|
1047
|
+
|
|
1048
|
+
def __del__(self) -> None:
|
|
1049
|
+
self.close()
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
@dataclass(frozen=True)
|
|
1053
|
+
class UdpStart:
|
|
1054
|
+
"""A UDP follow start position. Use the named constructors."""
|
|
1055
|
+
|
|
1056
|
+
kind: int
|
|
1057
|
+
value: int = 0
|
|
1058
|
+
|
|
1059
|
+
@classmethod
|
|
1060
|
+
def new(cls) -> UdpStart:
|
|
1061
|
+
return cls(0)
|
|
1062
|
+
|
|
1063
|
+
@classmethod
|
|
1064
|
+
def last(cls, count: int) -> UdpStart:
|
|
1065
|
+
return cls(1, _ensure_non_negative_int(count, "count"))
|
|
1066
|
+
|
|
1067
|
+
@classmethod
|
|
1068
|
+
def since_ns(cls, timestamp_ns: int) -> UdpStart:
|
|
1069
|
+
return cls(2, _ensure_non_negative_int(timestamp_ns, "timestamp_ns"))
|
|
1070
|
+
|
|
1071
|
+
@classmethod
|
|
1072
|
+
def sequence(cls, seq: int) -> UdpStart:
|
|
1073
|
+
return cls(3, _ensure_non_negative_int(seq, "seq"))
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
@dataclass(frozen=True)
|
|
1077
|
+
class UdpFollowOptions:
|
|
1078
|
+
start: UdpStart = UdpStart(0)
|
|
1079
|
+
live: bool = True
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@dataclass(frozen=True)
|
|
1083
|
+
class UdpFrame:
|
|
1084
|
+
seq: int
|
|
1085
|
+
timestamp_ns: int
|
|
1086
|
+
payload: bytes
|
|
1087
|
+
|
|
1088
|
+
@property
|
|
1089
|
+
def time(self) -> datetime:
|
|
1090
|
+
seconds, nanoseconds = divmod(self.timestamp_ns, 1_000_000_000)
|
|
1091
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(
|
|
1092
|
+
microsecond=nanoseconds // 1_000
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
def message(self) -> Message:
|
|
1096
|
+
payload_buf = (c_uint8 * len(self.payload)).from_buffer_copy(self.payload)
|
|
1097
|
+
event = plsm_udp_event_t()
|
|
1098
|
+
event.kind = 1
|
|
1099
|
+
event.seq = self.seq
|
|
1100
|
+
event.timestamp_ns = self.timestamp_ns
|
|
1101
|
+
event.payload.data = cast(payload_buf, c_void_p) if self.payload else None
|
|
1102
|
+
event.payload.len = len(self.payload)
|
|
1103
|
+
out_message = plsm_buf_t()
|
|
1104
|
+
out_err = POINTER(plsm_error_t)()
|
|
1105
|
+
rc = _LIB.plsm_udp_event_message(byref(event), byref(out_message), byref(out_err))
|
|
1106
|
+
if rc != 0:
|
|
1107
|
+
raise _take_error(out_err)
|
|
1108
|
+
return parse_message(_buf_to_bytes(out_message))
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
@dataclass(frozen=True)
|
|
1112
|
+
class UdpGap:
|
|
1113
|
+
first: int
|
|
1114
|
+
last: int
|
|
1115
|
+
reason: str
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
@dataclass(frozen=True)
|
|
1119
|
+
class UdpProgress:
|
|
1120
|
+
start_seq: int
|
|
1121
|
+
next_seq: int
|
|
1122
|
+
end_seq: int | None
|
|
1123
|
+
terminal: bool
|
|
1124
|
+
retention_gap: bool
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
UdpEvent = UdpFrame | UdpGap | UdpProgress
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
class UdpClient:
|
|
1131
|
+
"""Client for one explicit UDP endpoint."""
|
|
1132
|
+
|
|
1133
|
+
def __init__(self, endpoint: str) -> None:
|
|
1134
|
+
self._lock = RLock()
|
|
1135
|
+
self._ptr = None
|
|
1136
|
+
if not endpoint:
|
|
1137
|
+
raise ValueError("endpoint is required")
|
|
1138
|
+
out_client = POINTER(plsm_udp_client_t)()
|
|
1139
|
+
out_err = POINTER(plsm_error_t)()
|
|
1140
|
+
rc = _LIB.plsm_udp_client_new(endpoint.encode("utf-8"), byref(out_client), byref(out_err))
|
|
1141
|
+
if rc != 0:
|
|
1142
|
+
raise _take_error(out_err)
|
|
1143
|
+
self._ptr = out_client
|
|
1144
|
+
|
|
1145
|
+
@_serialized_handle
|
|
1146
|
+
def open_pool(self, pool_name: str) -> UdpPool:
|
|
1147
|
+
_require_open(self._ptr, "UDP client")
|
|
1148
|
+
if not pool_name:
|
|
1149
|
+
raise ValueError("pool_name is required")
|
|
1150
|
+
out_pool = POINTER(plsm_udp_pool_t)()
|
|
1151
|
+
out_err = POINTER(plsm_error_t)()
|
|
1152
|
+
rc = _LIB.plsm_udp_client_open_pool(
|
|
1153
|
+
self._ptr,
|
|
1154
|
+
pool_name.encode("utf-8"),
|
|
1155
|
+
byref(out_pool),
|
|
1156
|
+
byref(out_err),
|
|
1157
|
+
)
|
|
1158
|
+
if rc != 0:
|
|
1159
|
+
raise _take_error(out_err)
|
|
1160
|
+
return UdpPool(out_pool)
|
|
1161
|
+
|
|
1162
|
+
@_serialized_handle
|
|
1163
|
+
def close(self) -> None:
|
|
1164
|
+
if self._ptr:
|
|
1165
|
+
_LIB.plsm_udp_client_free(self._ptr)
|
|
1166
|
+
self._ptr = None
|
|
1167
|
+
|
|
1168
|
+
def __enter__(self) -> UdpClient:
|
|
1169
|
+
with self._lock:
|
|
1170
|
+
_require_open(self._ptr, "UDP client")
|
|
1171
|
+
return self
|
|
1172
|
+
|
|
1173
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
1174
|
+
self.close()
|
|
1175
|
+
|
|
1176
|
+
def __del__(self) -> None:
|
|
1177
|
+
self.close()
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
class UdpPool:
|
|
1181
|
+
"""A named pool on one UDP endpoint."""
|
|
1182
|
+
|
|
1183
|
+
def __init__(self, ptr: POINTER(plsm_udp_pool_t)) -> None:
|
|
1184
|
+
self._lock = RLock()
|
|
1185
|
+
self._ptr = ptr
|
|
1186
|
+
|
|
1187
|
+
@_serialized_handle
|
|
1188
|
+
def send_json(self, value: Any, tags: Iterable[str] = ()) -> None:
|
|
1189
|
+
_require_open(self._ptr, "UDP pool")
|
|
1190
|
+
payload = json.dumps(value, separators=(",", ":")).encode("utf-8")
|
|
1191
|
+
payload_buf = (c_uint8 * len(payload)).from_buffer_copy(payload)
|
|
1192
|
+
tag_arr, keepalive = _tag_array(tags)
|
|
1193
|
+
out_err = POINTER(plsm_error_t)()
|
|
1194
|
+
rc = _LIB.plsm_udp_pool_send_json(
|
|
1195
|
+
self._ptr,
|
|
1196
|
+
payload_buf,
|
|
1197
|
+
c_size_t(len(payload)),
|
|
1198
|
+
tag_arr,
|
|
1199
|
+
c_size_t(len(keepalive)),
|
|
1200
|
+
byref(out_err),
|
|
1201
|
+
)
|
|
1202
|
+
if rc != 0:
|
|
1203
|
+
raise _take_error(out_err)
|
|
1204
|
+
|
|
1205
|
+
@_serialized_handle
|
|
1206
|
+
def send_lite3(self, payload: bytes | bytearray | memoryview) -> None:
|
|
1207
|
+
_require_open(self._ptr, "UDP pool")
|
|
1208
|
+
payload = _ensure_bytes(payload, "payload")
|
|
1209
|
+
if not payload:
|
|
1210
|
+
raise ValueError("payload is required")
|
|
1211
|
+
payload_buf = (c_uint8 * len(payload)).from_buffer_copy(payload)
|
|
1212
|
+
out_err = POINTER(plsm_error_t)()
|
|
1213
|
+
rc = _LIB.plsm_udp_pool_send_lite3(
|
|
1214
|
+
self._ptr,
|
|
1215
|
+
payload_buf,
|
|
1216
|
+
c_size_t(len(payload)),
|
|
1217
|
+
byref(out_err),
|
|
1218
|
+
)
|
|
1219
|
+
if rc != 0:
|
|
1220
|
+
raise _take_error(out_err)
|
|
1221
|
+
|
|
1222
|
+
@_serialized_handle
|
|
1223
|
+
def follow(self, options: UdpFollowOptions | None = None) -> UdpFollow:
|
|
1224
|
+
_require_open(self._ptr, "UDP pool")
|
|
1225
|
+
options = options or UdpFollowOptions()
|
|
1226
|
+
if not isinstance(options, UdpFollowOptions):
|
|
1227
|
+
raise TypeError("options must be UdpFollowOptions")
|
|
1228
|
+
start = options.start
|
|
1229
|
+
if not isinstance(start, UdpStart) or start.kind not in (0, 1, 2, 3):
|
|
1230
|
+
raise ValueError("invalid UDP follow start")
|
|
1231
|
+
start_value = _ensure_non_negative_int(start.value, "start.value")
|
|
1232
|
+
out_follow = POINTER(plsm_udp_follow_t)()
|
|
1233
|
+
out_err = POINTER(plsm_error_t)()
|
|
1234
|
+
rc = _LIB.plsm_udp_pool_follow(
|
|
1235
|
+
self._ptr,
|
|
1236
|
+
c_uint32(start.kind),
|
|
1237
|
+
c_uint64(start_value),
|
|
1238
|
+
c_uint32(1 if options.live else 0),
|
|
1239
|
+
byref(out_follow),
|
|
1240
|
+
byref(out_err),
|
|
1241
|
+
)
|
|
1242
|
+
if rc != 0:
|
|
1243
|
+
raise _take_error(out_err)
|
|
1244
|
+
return UdpFollow(out_follow)
|
|
1245
|
+
|
|
1246
|
+
@_serialized_handle
|
|
1247
|
+
def close(self) -> None:
|
|
1248
|
+
if self._ptr:
|
|
1249
|
+
_LIB.plsm_udp_pool_free(self._ptr)
|
|
1250
|
+
self._ptr = None
|
|
1251
|
+
|
|
1252
|
+
def __enter__(self) -> UdpPool:
|
|
1253
|
+
with self._lock:
|
|
1254
|
+
_require_open(self._ptr, "UDP pool")
|
|
1255
|
+
return self
|
|
1256
|
+
|
|
1257
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
1258
|
+
self.close()
|
|
1259
|
+
|
|
1260
|
+
def __del__(self) -> None:
|
|
1261
|
+
self.close()
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
class UdpFollow:
|
|
1265
|
+
"""Best-effort UDP event stream. Close cancels a blocked wait."""
|
|
1266
|
+
|
|
1267
|
+
def __init__(self, ptr: POINTER(plsm_udp_follow_t)) -> None:
|
|
1268
|
+
self._state_lock = RLock()
|
|
1269
|
+
self._read_lock = Lock()
|
|
1270
|
+
self._ptr = ptr
|
|
1271
|
+
self._closing = False
|
|
1272
|
+
self._cancelled = False
|
|
1273
|
+
|
|
1274
|
+
def next_event(self, timeout_ms: int | None = None) -> UdpEvent | None:
|
|
1275
|
+
timeout_ms = _optional_non_negative_int(timeout_ms, "timeout_ms")
|
|
1276
|
+
with self._read_lock:
|
|
1277
|
+
with self._state_lock:
|
|
1278
|
+
_require_open(self._ptr, "UDP follow")
|
|
1279
|
+
if self._closing or self._cancelled:
|
|
1280
|
+
return None
|
|
1281
|
+
ptr = self._ptr
|
|
1282
|
+
remaining = timeout_ms
|
|
1283
|
+
while True:
|
|
1284
|
+
event = plsm_udp_event_t()
|
|
1285
|
+
out_err = POINTER(plsm_error_t)()
|
|
1286
|
+
wait_ms = 1000 if remaining is None else remaining
|
|
1287
|
+
rc = _LIB.plsm_udp_follow_next(
|
|
1288
|
+
ptr,
|
|
1289
|
+
c_uint64(wait_ms),
|
|
1290
|
+
byref(event),
|
|
1291
|
+
byref(out_err),
|
|
1292
|
+
)
|
|
1293
|
+
if rc == 1:
|
|
1294
|
+
return _udp_event_to_python(event)
|
|
1295
|
+
if rc == 2 or rc == 3:
|
|
1296
|
+
with self._state_lock:
|
|
1297
|
+
self._cancelled = rc == 3
|
|
1298
|
+
return None
|
|
1299
|
+
if rc < 0:
|
|
1300
|
+
raise _take_error(out_err)
|
|
1301
|
+
if remaining is not None:
|
|
1302
|
+
if remaining == 0:
|
|
1303
|
+
return None
|
|
1304
|
+
remaining -= wait_ms
|
|
1305
|
+
if remaining <= 0:
|
|
1306
|
+
return None
|
|
1307
|
+
|
|
1308
|
+
def events(self) -> Generator[UdpEvent, None, None]:
|
|
1309
|
+
while True:
|
|
1310
|
+
event = self.next_event()
|
|
1311
|
+
if event is None:
|
|
1312
|
+
return
|
|
1313
|
+
yield event
|
|
1314
|
+
|
|
1315
|
+
def __iter__(self) -> UdpFollow:
|
|
1316
|
+
return self
|
|
1317
|
+
|
|
1318
|
+
def __next__(self) -> UdpEvent:
|
|
1319
|
+
event = self.next_event()
|
|
1320
|
+
if event is None:
|
|
1321
|
+
raise StopIteration
|
|
1322
|
+
return event
|
|
1323
|
+
|
|
1324
|
+
def cancel(self) -> None:
|
|
1325
|
+
with self._state_lock:
|
|
1326
|
+
if self._ptr and not self._cancelled:
|
|
1327
|
+
_LIB.plsm_udp_follow_cancel(self._ptr)
|
|
1328
|
+
self._cancelled = True
|
|
1329
|
+
|
|
1330
|
+
def close(self) -> None:
|
|
1331
|
+
with self._state_lock:
|
|
1332
|
+
ptr = self._ptr
|
|
1333
|
+
if not ptr:
|
|
1334
|
+
return
|
|
1335
|
+
self._closing = True
|
|
1336
|
+
_LIB.plsm_udp_follow_cancel(ptr)
|
|
1337
|
+
with self._read_lock:
|
|
1338
|
+
with self._state_lock:
|
|
1339
|
+
if self._ptr:
|
|
1340
|
+
_LIB.plsm_udp_follow_close(self._ptr)
|
|
1341
|
+
self._ptr = None
|
|
1342
|
+
self._cancelled = True
|
|
1343
|
+
|
|
1344
|
+
def __enter__(self) -> UdpFollow:
|
|
1345
|
+
with self._state_lock:
|
|
1346
|
+
_require_open(self._ptr, "UDP follow")
|
|
1347
|
+
return self
|
|
1348
|
+
|
|
1349
|
+
def __exit__(self, _exc_type, _exc, _tb) -> None:
|
|
1350
|
+
self.close()
|
|
1351
|
+
|
|
1352
|
+
def __del__(self) -> None:
|
|
1353
|
+
self.close()
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _udp_event_to_python(event: plsm_udp_event_t) -> UdpEvent:
|
|
1357
|
+
try:
|
|
1358
|
+
if event.kind == 1:
|
|
1359
|
+
payload = b""
|
|
1360
|
+
if event.payload.data and event.payload.len:
|
|
1361
|
+
payload = bytes((c_uint8 * event.payload.len).from_address(event.payload.data))
|
|
1362
|
+
return UdpFrame(int(event.seq), int(event.timestamp_ns), payload)
|
|
1363
|
+
if event.kind == 2:
|
|
1364
|
+
reasons = {0: "unknown", 1: "oversized", 2: "retention", 3: "pressure"}
|
|
1365
|
+
return UdpGap(int(event.first), int(event.last), reasons.get(int(event.reason), "unknown"))
|
|
1366
|
+
if event.kind == 3:
|
|
1367
|
+
return UdpProgress(
|
|
1368
|
+
int(event.start_seq),
|
|
1369
|
+
int(event.next_seq),
|
|
1370
|
+
int(event.end_seq) if event.has_end else None,
|
|
1371
|
+
bool(event.terminal),
|
|
1372
|
+
bool(event.retention_gap),
|
|
1373
|
+
)
|
|
1374
|
+
raise PlasmiteError(ErrorKind.INTERNAL, f"plasmite: unknown UDP event kind {event.kind}")
|
|
1375
|
+
finally:
|
|
1376
|
+
_LIB.plsm_udp_event_free(byref(event))
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _parse_rfc3339_utc(raw_time: str) -> datetime:
|
|
1380
|
+
# Python 3.10 accepts only 3 or 6 fractional digits. datetime keeps
|
|
1381
|
+
# microseconds; the original nanosecond timestamp remains in Message.raw.
|
|
1382
|
+
raw_time = re.sub(r"\.(\d+)", lambda match: "." + match[1][:6].ljust(6, "0"), raw_time)
|
|
1383
|
+
parsed = datetime.fromisoformat(raw_time.replace("Z", "+00:00"))
|
|
1384
|
+
if parsed.tzinfo is None:
|
|
1385
|
+
raise ValueError("message time must include timezone")
|
|
1386
|
+
return parsed.astimezone(timezone.utc)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def parse_message(payload: bytes) -> Message:
|
|
1390
|
+
raw = _ensure_bytes(payload, "payload")
|
|
1391
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
1392
|
+
if not isinstance(parsed, dict):
|
|
1393
|
+
raise ValueError("message payload must decode to an object")
|
|
1394
|
+
raw_time = parsed.get("time")
|
|
1395
|
+
if not isinstance(raw_time, str):
|
|
1396
|
+
raise ValueError("message time must be an RFC3339 string")
|
|
1397
|
+
meta = parsed.get("meta", {})
|
|
1398
|
+
tags = meta.get("tags") if isinstance(meta, dict) else None
|
|
1399
|
+
if not isinstance(tags, list):
|
|
1400
|
+
tags = []
|
|
1401
|
+
tag_list = [str(tag) for tag in tags]
|
|
1402
|
+
seq = parsed.get("seq")
|
|
1403
|
+
if not isinstance(seq, int):
|
|
1404
|
+
raise ValueError("message seq must be an int")
|
|
1405
|
+
return Message(
|
|
1406
|
+
seq=seq,
|
|
1407
|
+
time=_parse_rfc3339_utc(raw_time),
|
|
1408
|
+
time_rfc3339=raw_time,
|
|
1409
|
+
data=parsed.get("data"),
|
|
1410
|
+
meta=MessageMeta(tags=tag_list),
|
|
1411
|
+
raw=raw,
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def _message_has_tags(message: Message, required_tags: list[str]) -> bool:
|
|
1416
|
+
if not required_tags:
|
|
1417
|
+
return True
|
|
1418
|
+
return all(tag in message.tags for tag in required_tags)
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
__all__ = [
|
|
1422
|
+
"Client",
|
|
1423
|
+
"Pool",
|
|
1424
|
+
"Message",
|
|
1425
|
+
"MessageMeta",
|
|
1426
|
+
"Stream",
|
|
1427
|
+
"Lite3Frame",
|
|
1428
|
+
"Lite3Stream",
|
|
1429
|
+
"UdpClient",
|
|
1430
|
+
"UdpPool",
|
|
1431
|
+
"UdpFollow",
|
|
1432
|
+
"UdpStart",
|
|
1433
|
+
"UdpFollowOptions",
|
|
1434
|
+
"UdpFrame",
|
|
1435
|
+
"UdpGap",
|
|
1436
|
+
"UdpProgress",
|
|
1437
|
+
"Durability",
|
|
1438
|
+
"ErrorKind",
|
|
1439
|
+
"PlasmiteError",
|
|
1440
|
+
"NotFoundError",
|
|
1441
|
+
"AlreadyExistsError",
|
|
1442
|
+
"BusyError",
|
|
1443
|
+
"PermissionDeniedError",
|
|
1444
|
+
"CorruptError",
|
|
1445
|
+
"IoError",
|
|
1446
|
+
"UsageError",
|
|
1447
|
+
"InternalError",
|
|
1448
|
+
"RetentionGapError",
|
|
1449
|
+
"DEFAULT_POOL_DIR",
|
|
1450
|
+
"DEFAULT_POOL_SIZE",
|
|
1451
|
+
"DEFAULT_POOL_SIZE_BYTES",
|
|
1452
|
+
"default_pool_dir",
|
|
1453
|
+
"parse_message",
|
|
1454
|
+
]
|