pkstruct 0.1.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.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,504 @@
1
+ """
2
+ pkstruct.trees.btree
3
+ ====================
4
+ B-Tree implementation.
5
+
6
+ Architecture
7
+ ------------
8
+ ``BTree`` stores keys and values in multi-key nodes. Internal nodes have
9
+ children between every pair of keys. Insert triggers node splits when a
10
+ node overflows (exceeds ``2 * order - 1`` keys). Delete triggers merges or
11
+ borrowing when a node underflows (drops below ``order - 1`` keys).
12
+
13
+ All major operations run in O(log n) due to the tree's self-balancing
14
+ property (every leaf is at the same depth).
15
+
16
+ Public API
17
+ ----------
18
+ Core CRUD
19
+ insert, delete, search, contains, update, clear
20
+
21
+ Metrics
22
+ size, is_empty, min, max
23
+
24
+ Structural
25
+ validate
26
+
27
+ Traversal
28
+ __iter__, __reversed__ (in-order)
29
+
30
+ Dunder
31
+ __len__, __contains__, __iter__, __repr__
32
+
33
+ Complexity
34
+ ----------
35
+ Search O(log n)
36
+ Insert O(log n)
37
+ Delete O(log n)
38
+ Space O(n)
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from collections.abc import Iterator
44
+ from typing import Any
45
+
46
+ from pkstruct.shared.threading import StructureLock
47
+ from pkstruct.trees.exceptions import (
48
+ DuplicateKeyError,
49
+ EmptyTreeError,
50
+ InvalidOrderError,
51
+ KeyNotFoundError,
52
+ )
53
+ from pkstruct.trees.node import BTreeNode
54
+
55
+
56
+ class BTree:
57
+ """A balanced multi-way search tree (B-Tree).
58
+
59
+ Parameters
60
+ ----------
61
+ order:
62
+ Minimum degree of the tree (must be >= 2). Each node (except root)
63
+ must contain at least ``order - 1`` keys and at most
64
+ ``2 * order - 1`` keys.
65
+ allow_duplicates:
66
+ When *False* (default) duplicate keys update the stored value.
67
+ When *True* duplicates are rejected with ``DuplicateKeyError``.
68
+ """
69
+
70
+ def __init__(self, order: int = 3, allow_duplicates: bool = False) -> None:
71
+ if order < 2:
72
+ raise InvalidOrderError(order)
73
+ self._order: int = order
74
+ self._allow_duplicates: bool = allow_duplicates
75
+ self._root: BTreeNode = BTreeNode()
76
+ self._size: int = 0
77
+ self._lock: StructureLock = StructureLock()
78
+
79
+ # ------------------------------------------------------------------
80
+ # Properties
81
+ # ------------------------------------------------------------------
82
+
83
+ @property
84
+ def order(self) -> int:
85
+ """Minimum degree of this B-Tree."""
86
+ return self._order
87
+
88
+ # ------------------------------------------------------------------
89
+ # Core CRUD
90
+ # ------------------------------------------------------------------
91
+
92
+ def insert(self, key: Any, value: Any = None) -> None:
93
+ """Insert *key* with optional *value*.
94
+
95
+ Parameters
96
+ ----------
97
+ key:
98
+ Comparable key.
99
+ value:
100
+ Optional payload.
101
+
102
+ Raises
103
+ ------
104
+ DuplicateKeyError
105
+ If ``allow_duplicates=True`` and *key* already exists.
106
+ """
107
+ with self._lock:
108
+ # Check for duplicate
109
+ try:
110
+ existing = self._find(self._root, key)
111
+ if existing is not None:
112
+ if self._allow_duplicates:
113
+ raise DuplicateKeyError(key)
114
+ # Update value in-place
115
+ self._update_value(self._root, key, value)
116
+ return
117
+ except KeyNotFoundError:
118
+ pass
119
+
120
+ root = self._root
121
+ if len(root.keys) == 2 * self._order - 1:
122
+ new_root = BTreeNode()
123
+ new_root.children.append(root)
124
+ root.parent = new_root
125
+ self._split_child(new_root, 0)
126
+ self._root = new_root
127
+ self._insert_non_full(self._root, key, value)
128
+ self._size += 1
129
+
130
+ def _insert_non_full(self, node: BTreeNode, key: Any, value: Any) -> None:
131
+ i = len(node.keys) - 1
132
+ if node.is_leaf():
133
+ node.keys.append(None)
134
+ node.values.append(None)
135
+ while i >= 0 and key < node.keys[i]:
136
+ node.keys[i + 1] = node.keys[i]
137
+ node.values[i + 1] = node.values[i]
138
+ i -= 1
139
+ node.keys[i + 1] = key
140
+ node.values[i + 1] = value
141
+ else:
142
+ while i >= 0 and key < node.keys[i]:
143
+ i -= 1
144
+ i += 1
145
+ if len(node.children[i].keys) == 2 * self._order - 1:
146
+ self._split_child(node, i)
147
+ if key > node.keys[i]:
148
+ i += 1
149
+ self._insert_non_full(node.children[i], key, value)
150
+
151
+ def _split_child(self, parent: BTreeNode, index: int) -> None:
152
+ order = self._order
153
+ child = parent.children[index]
154
+ new_child = BTreeNode()
155
+ new_child.parent = parent
156
+ mid = order - 1
157
+
158
+ # Save the key/value to promote before truncating
159
+ mid_key = child.keys[mid]
160
+ mid_value = child.values[mid]
161
+
162
+ # Copy second half of keys/values
163
+ new_child.keys = child.keys[mid + 1 :]
164
+ new_child.values = child.values[mid + 1 :]
165
+ child.keys = child.keys[:mid]
166
+ child.values = child.values[:mid]
167
+
168
+ if not child.is_leaf():
169
+ new_child.children = child.children[mid + 1 :]
170
+ child.children = child.children[: mid + 1]
171
+
172
+ # Promote middle key to parent
173
+ parent.keys.insert(index, mid_key)
174
+ parent.values.insert(index, mid_value)
175
+ parent.children.insert(index + 1, new_child)
176
+
177
+ def delete(self, key: Any) -> None:
178
+ """Remove *key* from the tree.
179
+
180
+ Parameters
181
+ ----------
182
+ key:
183
+ Key to remove.
184
+
185
+ Raises
186
+ ------
187
+ KeyNotFoundError
188
+ If *key* is not present.
189
+ """
190
+ with self._lock:
191
+ if not self._find(self._root, key):
192
+ raise KeyNotFoundError(key)
193
+ self._delete(self._root, key)
194
+ self._size -= 1
195
+ if len(self._root.keys) == 0 and not self._root.is_leaf():
196
+ self._root = self._root.children[0]
197
+ self._root.parent = None
198
+
199
+ def _delete(self, node: BTreeNode, key: Any) -> None:
200
+ if node.is_leaf():
201
+ if key in node.keys:
202
+ idx = node.keys.index(key)
203
+ node.keys.pop(idx)
204
+ node.values.pop(idx)
205
+ return
206
+
207
+ if key in node.keys:
208
+ idx = node.keys.index(key)
209
+ if len(node.children[idx].keys) >= self._order:
210
+ pred = self._get_predecessor(node.children[idx])
211
+ node.keys[idx], node.values[idx] = pred
212
+ self._delete(node.children[idx], pred[0])
213
+ elif len(node.children[idx + 1].keys) >= self._order:
214
+ succ = self._get_successor(node.children[idx + 1])
215
+ node.keys[idx], node.values[idx] = succ
216
+ self._delete(node.children[idx + 1], succ[0])
217
+ else:
218
+ self._merge(node, idx)
219
+ self._delete(node.children[idx], key)
220
+ else:
221
+ idx = 0
222
+ while idx < len(node.keys) and key > node.keys[idx]:
223
+ idx += 1
224
+ if len(node.children[idx].keys) < self._order:
225
+ self._fix_child(node, idx)
226
+ if idx > 0 and node.children[idx - 1] is self._get_sibling(node, idx):
227
+ idx -= 1
228
+ self._delete(node.children[idx], key)
229
+
230
+ def _get_predecessor(self, node: BTreeNode) -> tuple[Any, Any]:
231
+ while not node.is_leaf():
232
+ node = node.children[-1]
233
+ return (node.keys[-1], node.values[-1])
234
+
235
+ def _get_successor(self, node: BTreeNode) -> tuple[Any, Any]:
236
+ while not node.is_leaf():
237
+ node = node.children[0]
238
+ return (node.keys[0], node.values[0])
239
+
240
+ def _merge(self, parent: BTreeNode, idx: int) -> None:
241
+ left = parent.children[idx]
242
+ right = parent.children[idx + 1]
243
+ left.keys.append(parent.keys.pop(idx))
244
+ left.values.append(parent.values.pop(idx))
245
+ left.keys.extend(right.keys)
246
+ left.values.extend(right.values)
247
+ if not right.is_leaf():
248
+ left.children.extend(right.children)
249
+ parent.children.pop(idx + 1)
250
+
251
+ def _fix_child(self, parent: BTreeNode, idx: int) -> None:
252
+ if idx > 0 and len(parent.children[idx - 1].keys) >= self._order:
253
+ self._borrow_from_left(parent, idx)
254
+ elif idx < len(parent.children) - 1 and len(parent.children[idx + 1].keys) >= self._order:
255
+ self._borrow_from_right(parent, idx)
256
+ else:
257
+ if idx < len(parent.children) - 1:
258
+ self._merge(parent, idx)
259
+ else:
260
+ self._merge(parent, idx - 1)
261
+
262
+ def _borrow_from_left(self, parent: BTreeNode, idx: int) -> None:
263
+ child = parent.children[idx]
264
+ left = parent.children[idx - 1]
265
+ child.keys.insert(0, parent.keys[idx - 1])
266
+ child.values.insert(0, parent.values[idx - 1])
267
+ parent.keys[idx - 1] = left.keys.pop()
268
+ parent.values[idx - 1] = left.values.pop()
269
+ if not left.is_leaf():
270
+ child.children.insert(0, left.children.pop())
271
+
272
+ def _borrow_from_right(self, parent: BTreeNode, idx: int) -> None:
273
+ child = parent.children[idx]
274
+ right = parent.children[idx + 1]
275
+ child.keys.append(parent.keys[idx])
276
+ child.values.append(parent.values[idx])
277
+ parent.keys[idx] = right.keys.pop(0)
278
+ parent.values[idx] = right.values.pop(0)
279
+ if not right.is_leaf():
280
+ child.children.append(right.children.pop(0))
281
+
282
+ def _get_sibling(self, node: BTreeNode, idx: int) -> BTreeNode:
283
+ if idx < len(node.children) - 1:
284
+ return node.children[idx + 1]
285
+ return node.children[idx - 1]
286
+
287
+ def search(self, key: Any) -> Any | None:
288
+ """Return the value associated with *key*, or *None* if absent."""
289
+ with self._lock:
290
+ try:
291
+ node, idx = self._search(self._root, key)
292
+ return node.values[idx]
293
+ except (KeyNotFoundError, TypeError):
294
+ return None
295
+
296
+ def _search(self, node: BTreeNode | None, key: Any) -> tuple[BTreeNode, int]:
297
+ if node is None:
298
+ raise KeyNotFoundError(key)
299
+ i = 0
300
+ while i < len(node.keys) and key > node.keys[i]:
301
+ i += 1
302
+ if i < len(node.keys) and key == node.keys[i]:
303
+ return node, i
304
+ if node.is_leaf():
305
+ raise KeyNotFoundError(key)
306
+ return self._search(node.children[i], key)
307
+
308
+ def contains(self, key: Any) -> bool:
309
+ """Return *True* if *key* is present."""
310
+ with self._lock:
311
+ try:
312
+ self._search(self._root, key)
313
+ return True
314
+ except KeyNotFoundError:
315
+ return False
316
+
317
+ def update(self, key: Any, value: Any) -> None:
318
+ """Update the value of an existing *key*.
319
+
320
+ Raises
321
+ ------
322
+ KeyNotFoundError
323
+ If *key* does not exist.
324
+ """
325
+ with self._lock:
326
+ node, idx = self._search(self._root, key)
327
+ node.values[idx] = value
328
+
329
+ def _update_value(self, node: BTreeNode, key: Any, value: Any) -> None:
330
+ try:
331
+ n, idx = self._search(node, key)
332
+ n.values[idx] = value
333
+ except KeyNotFoundError:
334
+ pass
335
+
336
+ def _find(self, node: BTreeNode | None, key: Any) -> BTreeNode | None:
337
+ try:
338
+ n, _ = self._search(node, key)
339
+ return n
340
+ except KeyNotFoundError:
341
+ return None
342
+
343
+ def clear(self) -> None:
344
+ """Remove all keys from the tree."""
345
+ with self._lock:
346
+ self._root = BTreeNode()
347
+ self._size = 0
348
+
349
+ # ------------------------------------------------------------------
350
+ # Metrics
351
+ # ------------------------------------------------------------------
352
+
353
+ def size(self) -> int:
354
+ """Return the number of keys currently in the tree."""
355
+ with self._lock:
356
+ return self._size
357
+
358
+ def is_empty(self) -> bool:
359
+ """Return *True* if the tree contains no keys."""
360
+ with self._lock:
361
+ return self._size == 0
362
+
363
+ def min(self) -> Any:
364
+ """Return the minimum key.
365
+
366
+ Raises
367
+ ------
368
+ EmptyTreeError
369
+ If the tree is empty.
370
+ """
371
+ with self._lock:
372
+ if self.is_empty():
373
+ raise EmptyTreeError("min")
374
+ node = self._root
375
+ while not node.is_leaf():
376
+ node = node.children[0]
377
+ return node.keys[0]
378
+
379
+ def max(self) -> Any:
380
+ """Return the maximum key.
381
+
382
+ Raises
383
+ ------
384
+ EmptyTreeError
385
+ If the tree is empty.
386
+ """
387
+ with self._lock:
388
+ if self.is_empty():
389
+ raise EmptyTreeError("max")
390
+ node = self._root
391
+ while not node.is_leaf():
392
+ node = node.children[-1]
393
+ return node.keys[-1]
394
+
395
+ # ------------------------------------------------------------------
396
+ # Validation
397
+ # ------------------------------------------------------------------
398
+
399
+ def validate(self) -> bool:
400
+ """Verify internal B-Tree invariants.
401
+
402
+ Checks:
403
+ 1. Every node (except root) has at least ``order - 1`` keys.
404
+ 2. Every node has at most ``2 * order - 1`` keys.
405
+ 3. All leaves are at the same depth.
406
+ 4. Keys within each node are sorted.
407
+ 5. BST ordering is maintained across children.
408
+
409
+ Returns
410
+ -------
411
+ bool
412
+ *True* if all invariants hold.
413
+ """
414
+ with self._lock:
415
+ if self._size == 0:
416
+ return True
417
+ try:
418
+ self._validate(self._root, None, None)
419
+ return True
420
+ except (ValueError, AssertionError):
421
+ return False
422
+
423
+ def _validate(
424
+ self,
425
+ node: BTreeNode | None,
426
+ lo: Any,
427
+ hi: Any,
428
+ depth: int | None = None,
429
+ leaf_depth: list | None = None,
430
+ ) -> None:
431
+ if node is None:
432
+ return
433
+ if leaf_depth is None:
434
+ leaf_depth = []
435
+ if depth is None:
436
+ depth = 0
437
+
438
+ # Key count invariants
439
+ kc = len(node.keys)
440
+ assert kc >= self._order - 1 or node.parent is None, (
441
+ f"Underflow: node has {kc} keys, need at least {self._order - 1}"
442
+ )
443
+ assert kc <= 2 * self._order - 1, (
444
+ f"Overflow: node has {kc} keys, max is {2 * self._order - 1}"
445
+ )
446
+
447
+ # Keys sorted
448
+ for i in range(kc - 1):
449
+ assert node.keys[i] < node.keys[i + 1], "Keys not sorted"
450
+
451
+ # BST ordering
452
+ for _, k in enumerate(node.keys):
453
+ if lo is not None and k <= lo:
454
+ raise ValueError(f"Key {k} <= lower bound {lo}")
455
+ if hi is not None and k >= hi:
456
+ raise ValueError(f"Key {k} >= upper bound {hi}")
457
+
458
+ if node.is_leaf():
459
+ leaf_depth.append(depth)
460
+ if len(leaf_depth) > 1 and leaf_depth[-1] != leaf_depth[-2]:
461
+ raise ValueError("Leaves at different depths")
462
+ return
463
+
464
+ # Children
465
+ assert len(node.children) == kc + 1, "Wrong child count"
466
+
467
+ for i, child in enumerate(node.children):
468
+ child_lo = lo if i == 0 else node.keys[i - 1]
469
+ child_hi = hi if i == kc else node.keys[i]
470
+ self._validate(child, child_lo, child_hi, depth + 1, leaf_depth)
471
+
472
+ # ------------------------------------------------------------------
473
+ # Dunder methods
474
+ # ------------------------------------------------------------------
475
+
476
+ def __len__(self) -> int:
477
+ """Return the number of keys in the tree."""
478
+ return self.size()
479
+
480
+ def __contains__(self, key: Any) -> bool:
481
+ """Support ``key in tree`` syntax."""
482
+ return self.contains(key)
483
+
484
+ def __iter__(self) -> Iterator[Any]:
485
+ """Iterate over keys in ascending order."""
486
+ with self._lock:
487
+ keys = list(self._inorder(self._root))
488
+ return iter(keys)
489
+
490
+ def _inorder(self, node: BTreeNode | None) -> Iterator[Any]:
491
+ if node is None:
492
+ return
493
+ if node.is_leaf():
494
+ yield from node.keys
495
+ return
496
+ for i in range(len(node.keys)):
497
+ yield from self._inorder(node.children[i])
498
+ yield node.keys[i]
499
+ yield from self._inorder(node.children[-1])
500
+
501
+ def __repr__(self) -> str: # pragma: no cover
502
+ with self._lock:
503
+ keys = list(self._inorder(self._root))
504
+ return f"BTree(size={self._size}, order={self._order}, keys={keys})"
@@ -0,0 +1,96 @@
1
+ """
2
+ pkstruct.trees.exceptions
3
+ =========================
4
+ Custom exception hierarchy for the trees module.
5
+
6
+ All tree-specific exceptions inherit from :class:`TreeError` so callers
7
+ can catch the entire family with a single ``except TreeError`` clause
8
+ while still being able to distinguish individual error types.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ __all__ = [
14
+ "TreeError",
15
+ "KeyNotFoundError",
16
+ "DuplicateKeyError",
17
+ "EmptyTreeError",
18
+ "InvalidOrderError",
19
+ "InvalidOperationError",
20
+ "TreeBalanceError",
21
+ "SerializationError",
22
+ "InvalidIntervalError",
23
+ "IndexOutOfBoundsError",
24
+ ]
25
+
26
+
27
+ class TreeError(Exception):
28
+ """Base exception for all tree-related errors."""
29
+
30
+
31
+ class KeyNotFoundError(TreeError):
32
+ """Raised when a requested key does not exist in the tree."""
33
+
34
+ def __init__(self, key: object) -> None:
35
+ super().__init__(f"Key not found: {key!r}")
36
+ self.key = key
37
+
38
+
39
+ class DuplicateKeyError(TreeError):
40
+ """Raised when inserting a key that already exists (where disallowed)."""
41
+
42
+ def __init__(self, key: object) -> None:
43
+ super().__init__(f"Duplicate key: {key!r}")
44
+ self.key = key
45
+
46
+
47
+ class EmptyTreeError(TreeError):
48
+ """Raised when an operation requires a non-empty tree."""
49
+
50
+ def __init__(self, operation: str = "operation") -> None:
51
+ super().__init__(f"Cannot perform '{operation}' on an empty tree.")
52
+ self.operation = operation
53
+
54
+
55
+ class InvalidOrderError(TreeError):
56
+ """Raised when a B-Tree / B+Tree order value is invalid."""
57
+
58
+ def __init__(self, order: int) -> None:
59
+ super().__init__(f"Tree order must be >= 2, got {order}.")
60
+ self.order = order
61
+
62
+
63
+ class InvalidOperationError(TreeError):
64
+ """Raised when an unsupported operation string is requested."""
65
+
66
+ def __init__(self, operation: str) -> None:
67
+ super().__init__(f"Unsupported operation: {operation!r}.")
68
+ self.operation = operation
69
+
70
+
71
+ class TreeBalanceError(TreeError):
72
+ """Raised when an internal balance invariant is violated."""
73
+
74
+
75
+ class SerializationError(TreeError):
76
+ """Raised when tree (de)serialization fails."""
77
+
78
+
79
+ class InvalidIntervalError(TreeError):
80
+ """Raised when an interval [lo, hi] has lo > hi."""
81
+
82
+ def __init__(self, lo: object, hi: object) -> None:
83
+ super().__init__(f"Invalid interval: [{lo!r}, {hi!r}] (lo > hi).")
84
+ self.lo = lo
85
+ self.hi = hi
86
+
87
+
88
+ class IndexOutOfBoundsError(TreeError):
89
+ """Raised when a k-th order statistic index is out of range."""
90
+
91
+ def __init__(self, index: int, size: int) -> None:
92
+ super().__init__(
93
+ f"Index {index} out of bounds for tree of size {size}."
94
+ )
95
+ self.index = index
96
+ self.size = size