weftcodec 1.2.0__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.
weftcodec/torch.py ADDED
@@ -0,0 +1,94 @@
1
+ """PyTorch-facing dataset over a mounted WeftFS directory.
2
+
3
+ ``torch`` is an optional dependency and is never imported at module import time.
4
+ Plain ``bytes`` access works without it; requesting a tensor raises a clear
5
+ ``ImportError`` when PyTorch is absent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+ from pathlib import Path
13
+
14
+
15
+ def _torch_module():
16
+ try:
17
+ return importlib.import_module("torch")
18
+ except ImportError as exc: # pragma: no cover - exercised without torch
19
+ raise ImportError(
20
+ "weftcodec.torch requires PyTorch; install it with "
21
+ "`pip install weftcodec[torch]` (or `pip install torch`)"
22
+ ) from exc
23
+
24
+
25
+ class WeftMapDataset:
26
+ """Deterministic map-style dataset over files in a WeftFS mount.
27
+
28
+ Parameters
29
+ ----------
30
+ root:
31
+ Directory served by a WeftFS mount (or any directory).
32
+ transform:
33
+ Optional callable applied to the raw ``bytes`` before the item is
34
+ returned or converted to a tensor.
35
+ dtype:
36
+ When set (for example ``torch.uint8`` or ``torch.float32``), items are
37
+ returned as ``torch.frombuffer`` tensors instead of ``bytes``.
38
+ shape:
39
+ Optional shape used to reshape a returned tensor. ``-1`` can be used
40
+ for one inferred dimension.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ root: "str | os.PathLike[str]",
46
+ transform=None,
47
+ *,
48
+ dtype=None,
49
+ shape: "tuple[int, ...] | None" = None,
50
+ ) -> None:
51
+ self.root = Path(root)
52
+ if not self.root.is_dir():
53
+ raise NotADirectoryError("dataset root is not a directory: %s" % self.root)
54
+ self.transform = transform
55
+ self.dtype = dtype
56
+ self.shape = tuple(shape) if shape is not None else None
57
+ self._entries = self._walk()
58
+
59
+ def _walk(self):
60
+ entries = []
61
+ for dirpath, dirnames, filenames in os.walk(self.root):
62
+ dirnames.sort()
63
+ for name in sorted(filenames):
64
+ full = Path(dirpath) / name
65
+ if full.is_file():
66
+ relative = full.relative_to(self.root).as_posix()
67
+ entries.append((relative, full))
68
+ entries.sort(key=lambda entry: entry[0])
69
+ return entries
70
+
71
+ def __len__(self) -> int:
72
+ return len(self._entries)
73
+
74
+ def paths(self) -> "list[str]":
75
+ return [relative for relative, _ in self._entries]
76
+
77
+ def __getitem__(self, index: int):
78
+ if index < 0:
79
+ index += len(self._entries)
80
+ if index < 0 or index >= len(self._entries):
81
+ raise IndexError("dataset index %d out of range" % index)
82
+ relative, full = self._entries[index]
83
+ with open(full, "rb") as handle:
84
+ data = handle.read()
85
+ if self.transform is not None:
86
+ data = self.transform(data)
87
+ if self.dtype is None:
88
+ return relative, data
89
+ torch = _torch_module()
90
+ # `bytes` is read-only; torch.frombuffer needs a writable buffer.
91
+ tensor = torch.frombuffer(bytearray(data), dtype=self.dtype)
92
+ if self.shape is not None:
93
+ tensor = tensor.reshape(self.shape)
94
+ return relative, tensor
weftcodec/varint.py ADDED
@@ -0,0 +1,88 @@
1
+ """Minimal unsigned LEB128 varints (WEFTCODEC-FORMAT §1).
2
+
3
+ The decoder mirrors the Rust implementation exactly:
4
+
5
+ * at most 10 bytes;
6
+ * a multi-byte encoding must not end in ``0x00`` (``NonCanonical``);
7
+ * the tenth byte must be ``0x00`` or ``0x01`` (``Malformed`` otherwise);
8
+ * truncated input is ``Malformed``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .errors import Malformed, NonCanonical
14
+
15
+ MAX_VARINT_LEN = 10
16
+ U64_MAX = (1 << 64) - 1
17
+
18
+
19
+ def encoded_len(value: int) -> int:
20
+ if value < 0 or value > U64_MAX:
21
+ raise Malformed("varint value is outside u64")
22
+ n = 1
23
+ while value >= 0x80:
24
+ value >>= 7
25
+ n += 1
26
+ return n
27
+
28
+
29
+ def encode(value: int, out: bytearray) -> None:
30
+ """Append the minimal LEB128 encoding of ``value`` to ``out``."""
31
+ if value < 0 or value > U64_MAX:
32
+ raise Malformed("varint value is outside u64")
33
+ v = value
34
+ while True:
35
+ byte = v & 0x7F
36
+ v >>= 7
37
+ if v == 0:
38
+ out.append(byte)
39
+ return
40
+ out.append(byte | 0x80)
41
+
42
+
43
+ def encode_bytes(value: int) -> bytes:
44
+ out = bytearray()
45
+ encode(value, out)
46
+ return bytes(out)
47
+
48
+
49
+ def decode(data: bytes, pos: int) -> "tuple[int, int]":
50
+ """Decode one varint at ``pos``; return ``(value, next_pos)``."""
51
+ value = 0
52
+ shift = 0
53
+ offset = pos
54
+ count = 0
55
+ while True:
56
+ if offset >= len(data):
57
+ raise Malformed("truncated varint")
58
+ byte = data[offset]
59
+ offset += 1
60
+ count += 1
61
+ if count > MAX_VARINT_LEN:
62
+ raise Malformed("varint longer than 10 bytes")
63
+ if count == MAX_VARINT_LEN and byte > 0x01:
64
+ raise Malformed("varint overflows u64")
65
+ low = byte & 0x7F
66
+ if shift < 64:
67
+ value |= low << shift
68
+ elif low != 0:
69
+ raise Malformed("varint overflows u64")
70
+ if byte & 0x80 == 0:
71
+ if count > 1 and byte == 0x00:
72
+ raise NonCanonical("non-minimal varint")
73
+ return value, offset
74
+ shift += 7
75
+ if shift > 63:
76
+ raise Malformed("varint exceeds 10 bytes")
77
+
78
+
79
+ def zigzag_encode(value: int) -> int:
80
+ """``(v << 1) ^ (v >> 63)`` on a 64-bit two's-complement pattern."""
81
+ if value < -(1 << 63) or value > (1 << 63) - 1:
82
+ raise Malformed("signed value is outside i64")
83
+ return ((value << 1) ^ (value >> 63)) & U64_MAX
84
+
85
+
86
+ def zigzag_decode(value: int) -> int:
87
+ """Inverse of :func:`zigzag_encode`."""
88
+ return (value >> 1) ^ -(value & 1)
weftcodec/wcb.py ADDED
@@ -0,0 +1,310 @@
1
+ """Canonical WCB bytes (WEFTCODEC-FORMAT §8).
2
+
3
+ WCB is a strict, explicit subset of deterministic CBOR. Python values map as:
4
+
5
+ =============== ==========================================
6
+ Python WCB
7
+ =============== ==========================================
8
+ ``None`` null (tag 0x00)
9
+ ``False`` false (0x01)
10
+ ``True`` true (0x02)
11
+ ``U64(n)`` unsigned int (0x03)
12
+ ``I64(n)`` signed int (0x04)
13
+ ``str`` text (0x05), UTF-8
14
+ ``bytes`` bytes (0x06)
15
+ ``list``/tuple array (0x07)
16
+ ``dict`` map (0x08), text keys only
17
+ ``Timestamp(n)`` timestamp (0x09), i64 nanoseconds since the epoch
18
+ =============== ==========================================
19
+
20
+ Tag ``0x0A..0xFF`` is ``NonCanonical`` in v1. Map keys are sorted by their
21
+ full encoded bytes, duplicates are rejected, and varints are minimal.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+
28
+ from . import varint
29
+ from .errors import LimitExceeded, Malformed, NonCanonical
30
+ from .limits import Limits
31
+
32
+ TAG_NULL = 0x00
33
+ TAG_FALSE = 0x01
34
+ TAG_TRUE = 0x02
35
+ TAG_U64 = 0x03
36
+ TAG_I64 = 0x04
37
+ TAG_TEXT = 0x05
38
+ TAG_BYTES = 0x06
39
+ TAG_ARRAY = 0x07
40
+ TAG_MAP = 0x08
41
+ TAG_TIMESTAMP = 0x09
42
+
43
+ I64_MIN = -(1 << 63)
44
+ I64_MAX = (1 << 63) - 1
45
+ U64_MAX = (1 << 64) - 1
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class U64:
50
+ """Unsigned 64-bit integer."""
51
+
52
+ value: int
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class I64:
57
+ """Signed 64-bit integer."""
58
+
59
+ value: int
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Timestamp:
64
+ """Signed 64-bit nanoseconds since the Unix epoch, UTC."""
65
+
66
+ nanos: int
67
+
68
+
69
+ def encode(value, limits: "Limits | None" = None) -> bytes:
70
+ """Encode ``value`` to canonical WCB bytes."""
71
+ encoder = _Encoder(limits or Limits())
72
+ out = bytearray()
73
+ encoder.value(value, 0, out)
74
+ return bytes(out)
75
+
76
+
77
+ def decode(data: bytes, limits: "Limits | None" = None):
78
+ """Decode canonical WCB bytes, rejecting every non-canonical form."""
79
+ if not data:
80
+ raise Malformed("empty WCB value")
81
+ decoder = _Decoder(data, limits or Limits())
82
+ value = decoder.value(0)
83
+ if decoder.pos != len(data):
84
+ raise NonCanonical(
85
+ "trailing bytes after WCB value: %d of %d unconsumed"
86
+ % (len(data) - decoder.pos, len(data))
87
+ )
88
+ return value
89
+
90
+
91
+ class _Encoder:
92
+ __slots__ = ("limits", "items")
93
+
94
+ def __init__(self, limits: Limits) -> None:
95
+ self.limits = limits
96
+ self.items = 0
97
+
98
+ def _bump_item(self) -> None:
99
+ self.items += 1
100
+ if self.items > self.limits.max_structured_items:
101
+ raise LimitExceeded(
102
+ "WCB item count %d exceeds limit %d"
103
+ % (self.items, self.limits.max_structured_items)
104
+ )
105
+
106
+ def _check_text(self, length: int, what: str) -> None:
107
+ if length > self.limits.max_string_bytes:
108
+ raise LimitExceeded(
109
+ "%s length %d exceeds limit %d"
110
+ % (what, length, self.limits.max_string_bytes)
111
+ )
112
+
113
+ def _enter(self, depth: int) -> int:
114
+ nxt = depth + 1
115
+ if nxt > self.limits.max_structured_depth:
116
+ raise LimitExceeded(
117
+ "WCB nesting depth %d exceeds limit %d"
118
+ % (nxt, self.limits.max_structured_depth)
119
+ )
120
+ return nxt
121
+
122
+ def value(self, value, depth: int, out: bytearray) -> None:
123
+ self._bump_item()
124
+ if value is None:
125
+ out.append(TAG_NULL)
126
+ elif value is False:
127
+ out.append(TAG_FALSE)
128
+ elif value is True:
129
+ out.append(TAG_TRUE)
130
+ elif isinstance(value, U64):
131
+ if value.value < 0 or value.value > U64_MAX:
132
+ raise Malformed("U64 value is outside u64")
133
+ out.append(TAG_U64)
134
+ varint.encode(value.value, out)
135
+ elif isinstance(value, I64):
136
+ if value.value < I64_MIN or value.value > I64_MAX:
137
+ raise Malformed("I64 value is outside i64")
138
+ out.append(TAG_I64)
139
+ varint.encode(varint.zigzag_encode(value.value), out)
140
+ elif isinstance(value, Timestamp):
141
+ if value.nanos < I64_MIN or value.nanos > I64_MAX:
142
+ raise Malformed("Timestamp value is outside i64")
143
+ out.append(TAG_TIMESTAMP)
144
+ varint.encode(varint.zigzag_encode(value.nanos), out)
145
+ elif isinstance(value, str):
146
+ raw = value.encode("utf-8")
147
+ self._check_text(len(raw), "text")
148
+ out.append(TAG_TEXT)
149
+ varint.encode(len(raw), out)
150
+ out.extend(raw)
151
+ elif isinstance(value, (bytes, bytearray, memoryview)):
152
+ raw = bytes(value)
153
+ self._check_text(len(raw), "bytes")
154
+ out.append(TAG_BYTES)
155
+ varint.encode(len(raw), out)
156
+ out.extend(raw)
157
+ elif isinstance(value, (list, tuple)):
158
+ child = self._enter(depth)
159
+ if len(value) > self.limits.max_structured_items:
160
+ raise LimitExceeded("WCB array too large")
161
+ out.append(TAG_ARRAY)
162
+ varint.encode(len(value), out)
163
+ for item in value:
164
+ self.value(item, child, out)
165
+ elif isinstance(value, dict):
166
+ child = self._enter(depth)
167
+ if len(value) > self.limits.max_structured_items:
168
+ raise LimitExceeded("WCB map too large")
169
+ entries = []
170
+ for key in value:
171
+ if not isinstance(key, str):
172
+ raise Malformed("WCB map keys must be text")
173
+ raw = key.encode("utf-8")
174
+ self._check_text(len(raw), "map key")
175
+ encoded_key = bytearray()
176
+ encoded_key.append(TAG_TEXT)
177
+ varint.encode(len(raw), encoded_key)
178
+ encoded_key.extend(raw)
179
+ entries.append((bytes(encoded_key), key))
180
+ entries.sort(key=lambda entry: entry[0])
181
+ out.append(TAG_MAP)
182
+ varint.encode(len(entries), out)
183
+ for encoded_key, key in entries:
184
+ out.extend(encoded_key)
185
+ self.value(value[key], child, out)
186
+ else:
187
+ raise Malformed("value of type %s is not representable in WCB" % type(value).__name__)
188
+
189
+
190
+ class _Decoder:
191
+ __slots__ = ("input", "pos", "limits", "items")
192
+
193
+ def __init__(self, data: bytes, limits: Limits) -> None:
194
+ self.input = data
195
+ self.pos = 0
196
+ self.limits = limits
197
+ self.items = 0
198
+
199
+ def _bump_item(self) -> None:
200
+ self.items += 1
201
+ if self.items > self.limits.max_structured_items:
202
+ raise LimitExceeded(
203
+ "WCB item count %d exceeds limit %d"
204
+ % (self.items, self.limits.max_structured_items)
205
+ )
206
+
207
+ def _read_u8(self) -> int:
208
+ if self.pos >= len(self.input):
209
+ raise Malformed("truncated WCB value")
210
+ byte = self.input[self.pos]
211
+ self.pos += 1
212
+ return byte
213
+
214
+ def _read_varint(self) -> int:
215
+ value, self.pos = varint.decode(self.input, self.pos)
216
+ return value
217
+
218
+ def _remaining(self) -> int:
219
+ return len(self.input) - self.pos
220
+
221
+ def _take(self, length: int, what: str) -> bytes:
222
+ if length > self.limits.max_string_bytes:
223
+ raise LimitExceeded(
224
+ "%s length %d exceeds limit %d"
225
+ % (what, length, self.limits.max_string_bytes)
226
+ )
227
+ if length > self._remaining():
228
+ raise Malformed("truncated WCB %s" % what)
229
+ start = self.pos
230
+ self.pos += length
231
+ return self.input[start : self.pos]
232
+
233
+ def _enter(self, depth: int) -> int:
234
+ nxt = depth + 1
235
+ if nxt > self.limits.max_structured_depth:
236
+ raise LimitExceeded(
237
+ "WCB nesting depth %d exceeds limit %d"
238
+ % (nxt, self.limits.max_structured_depth)
239
+ )
240
+ return nxt
241
+
242
+ def _check_count(self, count: int, min_bytes_each: int, what: str) -> int:
243
+ if count > self.limits.max_structured_items:
244
+ raise LimitExceeded(
245
+ "WCB %s count %d exceeds limit %d"
246
+ % (what, count, self.limits.max_structured_items)
247
+ )
248
+ if count * min_bytes_each > self._remaining():
249
+ raise Malformed(
250
+ "truncated WCB %s: count %d cannot fit in %d remaining bytes"
251
+ % (what, count, self._remaining())
252
+ )
253
+ return count
254
+
255
+ def value(self, depth: int):
256
+ self._bump_item()
257
+ tag = self._read_u8()
258
+ if tag == TAG_NULL:
259
+ return None
260
+ if tag == TAG_FALSE:
261
+ return False
262
+ if tag == TAG_TRUE:
263
+ return True
264
+ if tag == TAG_U64:
265
+ return U64(self._read_varint())
266
+ if tag == TAG_I64:
267
+ return I64(varint.zigzag_decode(self._read_varint()))
268
+ if tag == TAG_TIMESTAMP:
269
+ return Timestamp(varint.zigzag_decode(self._read_varint()))
270
+ if tag == TAG_TEXT:
271
+ raw = self._take(self._read_varint(), "text")
272
+ try:
273
+ return raw.decode("utf-8")
274
+ except UnicodeDecodeError:
275
+ raise NonCanonical("WCB text is not valid UTF-8")
276
+ if tag == TAG_BYTES:
277
+ return self._take(self._read_varint(), "bytes")
278
+ if tag == TAG_ARRAY:
279
+ child = self._enter(depth)
280
+ count = self._check_count(self._read_varint(), 1, "array")
281
+ return [self.value(child) for _ in range(count)]
282
+ if tag == TAG_MAP:
283
+ child = self._enter(depth)
284
+ count = self._check_count(self._read_varint(), 2, "map")
285
+ result = {}
286
+ previous_key = None
287
+ for _ in range(count):
288
+ key_start = self.pos
289
+ key_tag = self._read_u8()
290
+ if key_tag != TAG_TEXT:
291
+ raise NonCanonical("WCB map key is not a text value (tag 0x05)")
292
+ raw = self._take(self._read_varint(), "map key")
293
+ key_end = self.pos
294
+ if previous_key is not None:
295
+ previous = self.input[previous_key[0] : previous_key[1]]
296
+ current = self.input[key_start:key_end]
297
+ if current <= previous:
298
+ raise NonCanonical(
299
+ "WCB map keys are duplicated or not sorted by encoded key bytes"
300
+ )
301
+ previous_key = (key_start, key_end)
302
+ try:
303
+ key = raw.decode("utf-8")
304
+ except UnicodeDecodeError:
305
+ raise NonCanonical("WCB map key is not valid UTF-8")
306
+ if key in result:
307
+ raise NonCanonical("duplicate WCB map key")
308
+ result[key] = self.value(child)
309
+ return result
310
+ raise NonCanonical("unknown WCB tag 0x%02x" % tag)