weida 0.1.0a16__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.
weida/__init__.py ADDED
@@ -0,0 +1,349 @@
1
+ """weida for Python, over the C ABI (`libweida`) with cffi.
2
+
3
+ The API mirrors the C ABI (docs/c/README.md): one class per handle, one
4
+ method per function. This module is the synchronous surface;
5
+ `weida.asyncio` has the same classes with coroutines for every method that
6
+ waits, and both share the value classes defined here.
7
+ """
8
+
9
+ from weida._enums import (
10
+ Acknowledgement,
11
+ Alpn,
12
+ Ask,
13
+ Backpressure,
14
+ Congestion,
15
+ Deduplication,
16
+ DeliveryLevel,
17
+ Discovery,
18
+ Durability,
19
+ ErrorCode,
20
+ GiveUp,
21
+ IdentityEventKind,
22
+ IncomingKind,
23
+ LossCause,
24
+ OrderingMode,
25
+ OutboxFull,
26
+ PeerEventKind,
27
+ ProducerNaming,
28
+ ReceivedKind,
29
+ ReportMode,
30
+ )
31
+ from weida._blob import Bits, Blob, PieceList, TreeHasher, blob_id_of
32
+ from weida._errors import (
33
+ AlreadyRegistered,
34
+ Again,
35
+ Canceled,
36
+ Closed,
37
+ ConnectionLost,
38
+ DatagramsUnavailable,
39
+ Expired,
40
+ Indeterminate,
41
+ InvalidAddress,
42
+ InvalidArgument,
43
+ InvalidEndpointPath,
44
+ InvalidFingerprint,
45
+ Interrupted,
46
+ Io,
47
+ LimitExceeded,
48
+ Negotiation,
49
+ NoParkedConnection,
50
+ NoReply,
51
+ NotConnected,
52
+ Panic,
53
+ Protocol,
54
+ Rejected,
55
+ RuntimeFailure,
56
+ Tls,
57
+ TooLarge,
58
+ Transport,
59
+ UnknownEndpoint,
60
+ Unsupported,
61
+ Untrusted,
62
+ WeidaError,
63
+ )
64
+ from weida._errors import check as _check
65
+ from weida._native import ffi as _ffi
66
+ from weida._native import lib as _lib
67
+ from weida._resolver import Lookup
68
+ from weida._stats import (
69
+ ConnectionStats,
70
+ PathStats,
71
+ PeerEvent,
72
+ RemoteStats,
73
+ TransportStats,
74
+ UdpCounts,
75
+ )
76
+ from weida._sync import (
77
+ Acceptor,
78
+ Binding,
79
+ BusMember,
80
+ Consumer,
81
+ Cursors,
82
+ Delivery,
83
+ Dish,
84
+ FanOut,
85
+ Fetch,
86
+ Flow,
87
+ Handoff,
88
+ IdentityEvents,
89
+ Incoming,
90
+ IncomingFlow,
91
+ IncomingRequest,
92
+ IncomingTransfer,
93
+ Listener,
94
+ LocalBinding,
95
+ Order,
96
+ OutgoingTransfer,
97
+ Paired,
98
+ Peer,
99
+ PeerEvents,
100
+ Piece,
101
+ PipeBinding,
102
+ Publisher,
103
+ Puller,
104
+ Pusher,
105
+ Radio,
106
+ Received,
107
+ Relay,
108
+ RelayRequest,
109
+ Replier,
110
+ ReplyStream,
111
+ Reporter,
112
+ Requester,
113
+ Respondent,
114
+ Runtime,
115
+ SeedSession,
116
+ Seeder,
117
+ Segment,
118
+ Subscriber,
119
+ SurveyRun,
120
+ Surveyor,
121
+ Swarm,
122
+ UnixBinding,
123
+ )
124
+ from weida._values import (
125
+ ANY_KEY,
126
+ ClientTls,
127
+ CursorEntry,
128
+ CursorSet,
129
+ DishDrops,
130
+ Drained,
131
+ FlowMeta,
132
+ FlowStats,
133
+ Gap,
134
+ GuaranteeSet,
135
+ Identity,
136
+ IdentityEvent,
137
+ IdentitySource,
138
+ IncomingFlowStats,
139
+ IncomingMeta,
140
+ Limits,
141
+ LocalPrincipal,
142
+ Message,
143
+ ReconnectPolicy,
144
+ RuntimeConfig,
145
+ ServerTls,
146
+ TopicDrops,
147
+ TransferMeta,
148
+ Trust,
149
+ TrustSource,
150
+ WeakIdentitySource,
151
+ WeakTrustSource,
152
+ WindowsPrincipal,
153
+ )
154
+
155
+
156
+ def version() -> str:
157
+ """The version of the loaded `libweida`."""
158
+ return _ffi.string(_lib.weida_version()).decode()
159
+
160
+
161
+ def strerror(code: int) -> str:
162
+ """The C ABI's text for a status code, positive or negative; "unknown
163
+ error" for any other."""
164
+ return _ffi.string(_lib.weida_strerror(code)).decode()
165
+
166
+
167
+ def new_trace() -> str:
168
+ """A fresh sampled W3C traceparent for `TransferMeta(trace=...)`; weida
169
+ never starts a trace on its own."""
170
+ buffer = _ffi.new("char[]", _lib.WEIDA_TRACEPARENT_LEN + 1)
171
+ _check(_lib.weida_new_trace(buffer, len(buffer)))
172
+ return _ffi.string(buffer).decode()
173
+
174
+
175
+ # The `Limits.datagram_receive_bytes` a profile with flows uses.
176
+ DEFAULT_DATAGRAM_RECEIVE_BYTES: int = _lib.WEIDA_DEFAULT_DATAGRAM_RECEIVE_BYTES
177
+
178
+ # How many layers a segment carries: 0 to MAX_SEGMENT_LAYERS - 1.
179
+ MAX_SEGMENT_LAYERS: int = _lib.WEIDA_MAX_SEGMENT_LAYERS
180
+
181
+ # The first cursor level an application may name; the levels below it are
182
+ # weida's own, the `Acknowledgement` members.
183
+ CURSOR_LEVEL_APPLICATION_FLOOR: int = _lib.WEIDA_CURSOR_LEVEL_APPLICATION_FLOOR
184
+
185
+ # The bytes of a blob's root, its BLAKE3 hash.
186
+ BLOB_ID_LEN: int = _lib.WEIDA_BLOB_ID_LEN
187
+
188
+ # The most pieces a blob may have.
189
+ MAX_PIECES: int = _lib.WEIDA_MAX_PIECES
190
+
191
+ # The smallest, the largest and the default piece size, as a power of two.
192
+ PIECE_SIZE_MIN_LOG2: int = _lib.WEIDA_PIECE_SIZE_MIN_LOG2
193
+ PIECE_SIZE_MAX_LOG2: int = _lib.WEIDA_PIECE_SIZE_MAX_LOG2
194
+ PIECE_SIZE_DEFAULT_LOG2: int = _lib.WEIDA_PIECE_SIZE_DEFAULT_LOG2
195
+
196
+
197
+ __all__ = [
198
+ # Values.
199
+ "ANY_KEY",
200
+ "Bits",
201
+ "Blob",
202
+ "ClientTls",
203
+ "ConnectionStats",
204
+ "CursorEntry",
205
+ "CursorSet",
206
+ "DishDrops",
207
+ "Drained",
208
+ "FlowMeta",
209
+ "FlowStats",
210
+ "Gap",
211
+ "GuaranteeSet",
212
+ "Identity",
213
+ "IdentityEvent",
214
+ "IdentitySource",
215
+ "Incoming",
216
+ "IncomingFlowStats",
217
+ "IncomingMeta",
218
+ "Limits",
219
+ "Lookup",
220
+ "LocalPrincipal",
221
+ "Message",
222
+ "Order",
223
+ "PathStats",
224
+ "PeerEvent",
225
+ "Piece",
226
+ "PieceList",
227
+ "Received",
228
+ "ReconnectPolicy",
229
+ "RemoteStats",
230
+ "RuntimeConfig",
231
+ "ServerTls",
232
+ "TopicDrops",
233
+ "TransferMeta",
234
+ "TransportStats",
235
+ "Trust",
236
+ "TreeHasher",
237
+ "TrustSource",
238
+ "UdpCounts",
239
+ "WeakIdentitySource",
240
+ "WeakTrustSource",
241
+ "WindowsPrincipal",
242
+ # Enumerations.
243
+ "Acknowledgement",
244
+ "Alpn",
245
+ "Ask",
246
+ "Backpressure",
247
+ "Congestion",
248
+ "Deduplication",
249
+ "DeliveryLevel",
250
+ "Discovery",
251
+ "Durability",
252
+ "ErrorCode",
253
+ "GiveUp",
254
+ "IdentityEventKind",
255
+ "IncomingKind",
256
+ "LossCause",
257
+ "OrderingMode",
258
+ "OutboxFull",
259
+ "PeerEventKind",
260
+ "ProducerNaming",
261
+ "ReceivedKind",
262
+ "ReportMode",
263
+ "BLOB_ID_LEN",
264
+ "CURSOR_LEVEL_APPLICATION_FLOOR",
265
+ "DEFAULT_DATAGRAM_RECEIVE_BYTES",
266
+ "MAX_PIECES",
267
+ "MAX_SEGMENT_LAYERS",
268
+ "PIECE_SIZE_DEFAULT_LOG2",
269
+ "PIECE_SIZE_MAX_LOG2",
270
+ "PIECE_SIZE_MIN_LOG2",
271
+ # Handles.
272
+ "Acceptor",
273
+ "Binding",
274
+ "BusMember",
275
+ "Consumer",
276
+ "Cursors",
277
+ "Delivery",
278
+ "Dish",
279
+ "FanOut",
280
+ "Fetch",
281
+ "Flow",
282
+ "Handoff",
283
+ "IdentityEvents",
284
+ "IncomingFlow",
285
+ "IncomingRequest",
286
+ "IncomingTransfer",
287
+ "Listener",
288
+ "LocalBinding",
289
+ "OutgoingTransfer",
290
+ "Paired",
291
+ "Peer",
292
+ "PeerEvents",
293
+ "PipeBinding",
294
+ "Publisher",
295
+ "Puller",
296
+ "Pusher",
297
+ "Radio",
298
+ "Relay",
299
+ "RelayRequest",
300
+ "Replier",
301
+ "ReplyStream",
302
+ "Reporter",
303
+ "Requester",
304
+ "Respondent",
305
+ "Runtime",
306
+ "SeedSession",
307
+ "Seeder",
308
+ "Segment",
309
+ "Subscriber",
310
+ "SurveyRun",
311
+ "Surveyor",
312
+ "Swarm",
313
+ "UnixBinding",
314
+ # Errors.
315
+ "WeidaError",
316
+ "AlreadyRegistered",
317
+ "Again",
318
+ "Canceled",
319
+ "Closed",
320
+ "ConnectionLost",
321
+ "DatagramsUnavailable",
322
+ "Expired",
323
+ "Indeterminate",
324
+ "InvalidAddress",
325
+ "InvalidArgument",
326
+ "InvalidEndpointPath",
327
+ "InvalidFingerprint",
328
+ "Interrupted",
329
+ "Io",
330
+ "LimitExceeded",
331
+ "Negotiation",
332
+ "NoParkedConnection",
333
+ "NoReply",
334
+ "NotConnected",
335
+ "Panic",
336
+ "Protocol",
337
+ "Rejected",
338
+ "RuntimeFailure",
339
+ "Tls",
340
+ "TooLarge",
341
+ "Transport",
342
+ "UnknownEndpoint",
343
+ "Unsupported",
344
+ "Untrusted",
345
+ "blob_id_of",
346
+ "new_trace",
347
+ "strerror",
348
+ "version",
349
+ ]
weida/_blob.py ADDED
@@ -0,0 +1,246 @@
1
+ """The piece tree of SWARM (docs/c/swarm.md): a blob's root and pieces, its
2
+ piece list, the tree hasher and piece maps, handles both surfaces share, and
3
+ the fetch options."""
4
+
5
+ from typing import Optional, Tuple
6
+
7
+ from weida._errors import check
8
+ from weida._native import ffi, lib
9
+ from weida._values import _count, _Fields, _Handle, _Made, _millis, _of
10
+
11
+ _U32_MAX = 2**32 - 1
12
+
13
+
14
+ def _bounded(name: str, value, maximum: int) -> int:
15
+ """`_count`, and at most `maximum`: the C parameter's range."""
16
+ value = _count(name, value)
17
+ if value > maximum:
18
+ raise ValueError(f"{name} must be at most {maximum}")
19
+ return value
20
+
21
+
22
+ def _bytes_arg(value):
23
+ """Any bytes-like as the `uint8_t` array C reads; cffi raises
24
+ `TypeError` for anything else."""
25
+ return ffi.from_buffer("uint8_t[]", value)
26
+
27
+
28
+ def blob_id_of(data) -> bytes:
29
+ """The id of `data` (any bytes-like): its BLAKE3 hash, `BLOB_ID_LEN` bytes."""
30
+ src = _bytes_arg(data)
31
+ out = ffi.new("uint8_t[]", lib.WEIDA_BLOB_ID_LEN)
32
+ check(lib.weida_blob_id_of(src, len(src), out))
33
+ return ffi.buffer(out)[:]
34
+
35
+
36
+ class Blob(_Handle):
37
+ """A blob of `size` bytes whose hash is `root`, cut into pieces of
38
+ `2**piece_log2` bytes."""
39
+
40
+ def __init__(self, root, size: int, piece_log2: int = lib.WEIDA_PIECE_SIZE_DEFAULT_LOG2) -> None:
41
+ """`root` is any bytes-like of `BLOB_ID_LEN` bytes, else a
42
+ `ValueError`. C refuses, with `InvalidArgument`, a `piece_log2`
43
+ outside 10..24, more than `MAX_PIECES` pieces, or an empty blob whose
44
+ root is not the hash of no bytes."""
45
+ r = _bytes_arg(root)
46
+ if len(r) != lib.WEIDA_BLOB_ID_LEN:
47
+ raise ValueError(f"root is {lib.WEIDA_BLOB_ID_LEN} bytes")
48
+ out = ffi.new("weida_blob **")
49
+ check(
50
+ lib.weida_blob_new(
51
+ r,
52
+ _bounded("size", size, 2**64 - 1),
53
+ _bounded("piece_log2", piece_log2, 255),
54
+ out,
55
+ )
56
+ )
57
+ super().__init__(out[0], lib.weida_blob_drop)
58
+
59
+ @classmethod
60
+ def _wrap(cls, ptr) -> "Blob":
61
+ blob = cls.__new__(cls)
62
+ _Handle.__init__(blob, ptr, lib.weida_blob_drop)
63
+ return blob
64
+
65
+ def root(self) -> bytes:
66
+ """The blob's id, its BLAKE3 hash: `BLOB_ID_LEN` bytes."""
67
+ out = ffi.new("uint8_t[]", lib.WEIDA_BLOB_ID_LEN)
68
+ check(lib.weida_blob_root(self._p(), out))
69
+ return ffi.buffer(out)[:]
70
+
71
+ def size(self) -> int:
72
+ """The blob's length in bytes."""
73
+ return lib.weida_blob_size(self._p())
74
+
75
+ def piece_log2(self) -> int:
76
+ """The piece size as a power of two."""
77
+ return lib.weida_blob_piece_log2(self._p())
78
+
79
+ def count(self) -> int:
80
+ """How many pieces the blob has."""
81
+ return lib.weida_blob_count(self._p())
82
+
83
+ def span(self, index: int) -> Optional[Tuple[int, int]]:
84
+ """The bytes piece `index` covers, `(start, end)` with `end`
85
+ exclusive; `None` past the last piece."""
86
+ start, end = ffi.new("uint64_t *"), ffi.new("uint64_t *")
87
+ if check(lib.weida_blob_span(self._p(), _bounded("index", index, _U32_MAX), start, end)) == 1:
88
+ return (start[0], end[0])
89
+ return None
90
+
91
+
92
+ class PieceList(_Made):
93
+ """A blob's piece list: one chaining value per piece, which checks each
94
+ piece against the root on its own."""
95
+
96
+ _DROP = lib.weida_piece_list_drop
97
+ _MADE_BY = "PieceList.of, PieceList.parse, TreeHasher.finish or Fetch.list"
98
+
99
+ @classmethod
100
+ def of(cls, data, piece_log2: int = lib.WEIDA_PIECE_SIZE_DEFAULT_LOG2) -> "PieceList":
101
+ """The piece list of `data` (any bytes-like) in one call."""
102
+ src = _bytes_arg(data)
103
+ out = ffi.new("weida_piece_list **")
104
+ check(lib.weida_piece_list_of(src, len(src), _bounded("piece_log2", piece_log2, 255), out))
105
+ return cls._wrap(out[0])
106
+
107
+ @classmethod
108
+ def parse(cls, blob: Blob, data) -> "PieceList":
109
+ """The piece list of `blob` from its bytes (`to_bytes()`); raises
110
+ `InvalidArgument` unless they are one chaining value per piece
111
+ hashing to the root."""
112
+ src = _bytes_arg(data)
113
+ out = ffi.new("weida_piece_list **")
114
+ check(lib.weida_piece_list_parse(_of("blob", blob, Blob), src, len(src), out))
115
+ return cls._wrap(out[0])
116
+
117
+ def blob(self) -> Blob:
118
+ """The blob this list belongs to."""
119
+ out = ffi.new("weida_blob **")
120
+ check(lib.weida_piece_list_blob(self._p(), out))
121
+ return Blob._wrap(out[0])
122
+
123
+ def to_bytes(self) -> bytes:
124
+ """The list's bytes, a copy; empty for a blob of at most one piece."""
125
+ length = ffi.new("size_t *")
126
+ ptr = lib.weida_piece_list_to_bytes(self._p(), length)
127
+ if length[0] == 0:
128
+ return b""
129
+ return ffi.buffer(ptr, length[0])[:]
130
+
131
+ def check(self, index: int, piece) -> bool:
132
+ """Whether `piece` (any bytes-like) is piece `index` of the blob."""
133
+ src = _bytes_arg(piece)
134
+ return check(lib.weida_piece_list_check(self._p(), _bounded("index", index, _U32_MAX), src, len(src))) == 1
135
+
136
+
137
+ class TreeHasher(_Handle):
138
+ """Hashes a blob fed in parts into its `PieceList`."""
139
+
140
+ def __init__(self, piece_log2: int = lib.WEIDA_PIECE_SIZE_DEFAULT_LOG2) -> None:
141
+ """A `piece_log2` outside 10..24 raises `InvalidArgument`."""
142
+ out = ffi.new("weida_tree_hasher **")
143
+ check(lib.weida_tree_hasher_new(_bounded("piece_log2", piece_log2, 255), out))
144
+ super().__init__(out[0], lib.weida_tree_hasher_drop)
145
+
146
+ def update(self, data) -> "TreeHasher":
147
+ """Feeds the next bytes (any bytes-like); returns this hasher."""
148
+ src = _bytes_arg(data)
149
+ check(lib.weida_tree_hasher_update(self._p(), src, len(src)))
150
+ return self
151
+
152
+ def finish(self) -> PieceList:
153
+ """The piece list of every byte fed. Consumes the hasher, also on
154
+ failure: more than `MAX_PIECES` pieces is `InvalidArgument`."""
155
+ out = ffi.new("weida_piece_list **")
156
+ check(lib.weida_tree_hasher_finish(self._consume(), out))
157
+ return PieceList._wrap(out[0])
158
+
159
+
160
+ class Bits(_Handle):
161
+ """A piece map: one bit per piece, all clear when made with `Bits(length)`."""
162
+
163
+ def __init__(self, length: int) -> None:
164
+ out = ffi.new("weida_bits **")
165
+ check(lib.weida_bits_new(_bounded("length", length, _U32_MAX), out))
166
+ super().__init__(out[0], lib.weida_bits_drop)
167
+
168
+ @classmethod
169
+ def _wrap(cls, ptr) -> "Bits":
170
+ bits = cls.__new__(cls)
171
+ _Handle.__init__(bits, ptr, lib.weida_bits_drop)
172
+ return bits
173
+
174
+ @classmethod
175
+ def full(cls, length: int) -> "Bits":
176
+ """A map of `length` bits, all set."""
177
+ out = ffi.new("weida_bits **")
178
+ check(lib.weida_bits_full(_bounded("length", length, _U32_MAX), out))
179
+ return cls._wrap(out[0])
180
+
181
+ @classmethod
182
+ def from_bytes(cls, length: int, data) -> "Bits":
183
+ """A map of `length` bits from `as_bytes()`'s form; `InvalidArgument`
184
+ unless `data` is `(length + 7) // 8` bytes with clear padding bits."""
185
+ n = _bounded("length", length, _U32_MAX)
186
+ src = _bytes_arg(data)
187
+ out = ffi.new("weida_bits **")
188
+ check(lib.weida_bits_from_bytes(n, src, len(src), out))
189
+ return cls._wrap(out[0])
190
+
191
+ def as_bytes(self) -> bytes:
192
+ """The map's bytes, a copy: bit `i` is bit `i % 8`, least significant
193
+ first, of byte `i // 8`."""
194
+ p = self._p()
195
+ n = (lib.weida_bits_len(p) + 7) // 8
196
+ buf = ffi.new("uint8_t[]", max(n, 1))
197
+ out_n = ffi.new("size_t *")
198
+ check(lib.weida_bits_as_bytes(p, buf, n, out_n))
199
+ return ffi.buffer(buf, out_n[0])[:]
200
+
201
+ def len(self) -> int:
202
+ """How many bits the map has."""
203
+ return lib.weida_bits_len(self._p())
204
+
205
+ def ones(self) -> int:
206
+ """How many bits are set."""
207
+ return lib.weida_bits_ones(self._p())
208
+
209
+ def get(self, index: int) -> bool:
210
+ """Whether bit `index` is set; `False` past the end."""
211
+ return lib.weida_bits_get(self._p(), _bounded("index", index, _U32_MAX)) == 1
212
+
213
+ def all(self) -> bool:
214
+ """Whether every bit is set."""
215
+ return lib.weida_bits_all(self._p()) == 1
216
+
217
+ def set(self, index: int, on: bool) -> None:
218
+ """Sets or clears bit `index`; past the end it does nothing."""
219
+ check(lib.weida_bits_set(self._p(), _bounded("index", index, _U32_MAX), 1 if on else 0))
220
+
221
+
222
+ class _FetchOptions(_Fields):
223
+ """`FetchOptions`, the keyword arguments of `Swarm.fetch`: `list` a
224
+ `PieceList`, `stall` and `have_refresh` seconds, the rest `int`s."""
225
+
226
+ _OUT = "weida_fetch_options **"
227
+ _NEW = lib.weida_fetch_options_new
228
+ _DROP = lib.weida_fetch_options_drop
229
+ _FIELDS = (
230
+ ("stall", lib.weida_fetch_options_set_stall_ms, _millis),
231
+ ("min_rate", lib.weida_fetch_options_set_min_rate, _count),
232
+ ("per_peer", lib.weida_fetch_options_set_per_peer, _count),
233
+ ("max_inflight", lib.weida_fetch_options_set_max_inflight, _count),
234
+ ("max_peers", lib.weida_fetch_options_set_max_peers, _count),
235
+ ("have_refresh", lib.weida_fetch_options_set_have_refresh_ms, _millis),
236
+ )
237
+
238
+ def _known(self) -> set:
239
+ return super()._known() | {"list"}
240
+
241
+ def _extra(self, handle) -> None:
242
+ if "list" in self._fields:
243
+ check(lib.weida_fetch_options_set_list(handle, _of("list", self._fields["list"], PieceList)))
244
+
245
+
246
+ __all__ = ["Bits", "Blob", "PieceList", "TreeHasher", "blob_id_of"]