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,148 @@
1
+ """Iterator utilities for pkstruct linked lists."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterator
5
+ from typing import Any
6
+
7
+
8
+ def _get_list_types() -> tuple[type, type, type]:
9
+ from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
10
+ from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
11
+ from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
12
+ return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
13
+
14
+
15
+ class ForwardIterator:
16
+ """Iterate over all node values in forward order.
17
+
18
+ Works with SinglyLinkedList, DoublyLinkedList, and CircularLinkedList.
19
+
20
+ Args:
21
+ list_instance: Any supported linked list instance.
22
+
23
+ Yields:
24
+ Node data values from head to tail.
25
+ """
26
+
27
+ def __init__(self, list_instance: Any) -> None:
28
+ self._list = list_instance
29
+
30
+ def __iter__(self) -> Iterator[Any]:
31
+ SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
32
+ lst = self._list
33
+
34
+ if isinstance(lst, CircularLinkedList):
35
+ size = lst.size
36
+ current = lst.head
37
+ steps = 0
38
+ while current is not None and steps < size:
39
+ yield current.value
40
+ current = current.next
41
+ steps += 1
42
+ elif isinstance(lst, (SinglyLinkedList, DoublyLinkedList)):
43
+ current = lst.head
44
+ visited: set[int] = set()
45
+ while current is not None:
46
+ if id(current) in visited:
47
+ break
48
+ visited.add(id(current))
49
+ yield current.value
50
+ current = current.next
51
+ else:
52
+ raise TypeError(
53
+ f"ForwardIterator does not support {type(lst).__name__}. "
54
+ "Use SinglyLinkedList, DoublyLinkedList, or CircularLinkedList."
55
+ )
56
+
57
+
58
+ class BackwardIterator:
59
+ """Iterate over all node values in reverse order.
60
+
61
+ Only supported for DoublyLinkedList (which has .prev pointers).
62
+
63
+ Args:
64
+ list_instance: A DoublyLinkedList instance.
65
+
66
+ Yields:
67
+ Node data values from tail to head.
68
+
69
+ Raises:
70
+ TypeError: If *list_instance* is not a DoublyLinkedList.
71
+ """
72
+
73
+ def __init__(self, list_instance: Any) -> None:
74
+ self._list = list_instance
75
+
76
+ def __iter__(self) -> Iterator[Any]:
77
+ SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
78
+ lst = self._list
79
+
80
+ if isinstance(lst, DoublyLinkedList):
81
+ current = lst.tail
82
+ visited: set[int] = set()
83
+ while current is not None:
84
+ if id(current) in visited:
85
+ break
86
+ visited.add(id(current))
87
+ yield current.value
88
+ current = current.prev
89
+ elif isinstance(lst, (SinglyLinkedList, CircularLinkedList)):
90
+ raise TypeError(
91
+ f"BackwardIterator is not supported for {type(lst).__name__}. "
92
+ "Only DoublyLinkedList supports backward iteration via .prev pointers."
93
+ )
94
+ else:
95
+ raise TypeError(
96
+ f"BackwardIterator does not support {type(lst).__name__}."
97
+ )
98
+
99
+
100
+ class CircularIterator:
101
+ """Infinite (or bounded) circular iterator over a CircularLinkedList.
102
+
103
+ Args:
104
+ list_instance: A CircularLinkedList instance.
105
+ max_cycles: Maximum full cycles to iterate before stopping.
106
+ ``None`` means infinite (caller is responsible for breaking).
107
+
108
+ Yields:
109
+ Node data values, wrapping around indefinitely (or until *max_cycles*
110
+ full circuits have been completed).
111
+
112
+ Raises:
113
+ TypeError: If *list_instance* is not a CircularLinkedList.
114
+ """
115
+
116
+ def __init__(self, list_instance: Any, max_cycles: int | None = None, max_steps: int | None = None) -> None:
117
+ self._list = list_instance
118
+ self._max_cycles = max_cycles
119
+ self._max_steps = max_steps
120
+
121
+ def __iter__(self) -> Iterator[Any]:
122
+ _, _, CircularLinkedList = _get_list_types()
123
+ lst = self._list
124
+
125
+ if not isinstance(lst, CircularLinkedList):
126
+ raise TypeError(
127
+ f"CircularIterator requires a CircularLinkedList, got {type(lst).__name__}."
128
+ )
129
+
130
+ if lst.head is None or lst.size == 0:
131
+ return
132
+
133
+ size = lst.size
134
+ if self._max_steps is not None:
135
+ limit = self._max_steps
136
+ elif self._max_cycles is not None:
137
+ limit = self._max_cycles * size
138
+ else:
139
+ limit = None
140
+
141
+ current = lst.head
142
+ steps = 0
143
+ while limit is None or steps < limit:
144
+ yield current.value
145
+ current = current.next
146
+ steps += 1
147
+ if current is None:
148
+ break
File without changes
@@ -0,0 +1,114 @@
1
+ """ASCII art visualization for linked list structures."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+
8
+ class AsciiVisualizer:
9
+ """Static methods for rendering linked lists as ASCII strings."""
10
+
11
+ @staticmethod
12
+ def singly(
13
+ head: Any,
14
+ node_repr: Callable[[Any], str] = str,
15
+ ) -> str:
16
+ """Render a singly linked list.
17
+
18
+ Args:
19
+ head: Head node (must have .value and .next attributes).
20
+ node_repr: Callable that converts a node's value to a string.
21
+
22
+ Returns:
23
+ String like ``"10 -> 20 -> 30 -> None"`` or ``"NULL (empty)"``
24
+ when the list is empty.
25
+ """
26
+ if head is None:
27
+ return "NULL (empty)"
28
+
29
+ parts: list[str] = []
30
+ current = head
31
+ visited: set[int] = set()
32
+
33
+ while current is not None:
34
+ node_id = id(current)
35
+ if node_id in visited:
36
+ parts.append("(cycle detected)")
37
+ break
38
+ visited.add(node_id)
39
+ parts.append(node_repr(current.value))
40
+ current = current.next
41
+
42
+ parts.append("None")
43
+ return " -> ".join(parts)
44
+
45
+ @staticmethod
46
+ def doubly(
47
+ head: Any,
48
+ tail: Any,
49
+ node_repr: Callable[[Any], str] = str,
50
+ ) -> str:
51
+ """Render a doubly linked list.
52
+
53
+ Args:
54
+ head: Head node (must have .value, .next, .prev attributes).
55
+ tail: Tail node.
56
+ node_repr: Callable that converts a node's value to a string.
57
+
58
+ Returns:
59
+ String like ``"None <- 10 <-> 20 <-> 30 -> None"`` or
60
+ ``"NULL <-> NULL"`` when the list is empty.
61
+ """
62
+ if head is None:
63
+ return "NULL <-> NULL"
64
+
65
+ parts: list[str] = []
66
+ current = head
67
+ visited: set[int] = set()
68
+
69
+ while current is not None:
70
+ node_id = id(current)
71
+ if node_id in visited:
72
+ parts.append("(cycle detected)")
73
+ break
74
+ visited.add(node_id)
75
+ parts.append(node_repr(current.value))
76
+ current = current.next
77
+
78
+ if not parts:
79
+ return "NULL <-> NULL"
80
+
81
+ inner = " <-> ".join(parts)
82
+ return f"None <- {inner} -> None"
83
+
84
+ @staticmethod
85
+ def circular(
86
+ head: Any,
87
+ size: int,
88
+ node_repr: Callable[[Any], str] = str,
89
+ ) -> str:
90
+ """Render a circular linked list.
91
+
92
+ Args:
93
+ head: Head node (must have .value and .next attributes).
94
+ size: Number of nodes (used as iteration guard).
95
+ node_repr: Callable that converts a node's value to a string.
96
+
97
+ Returns:
98
+ String like ``"10 -> 20 -> 30 -> (back to head)"`` or
99
+ ``"empty circular list"`` when the list is empty.
100
+ """
101
+ if head is None or size == 0:
102
+ return "empty circular list"
103
+
104
+ parts: list[str] = []
105
+ current = head
106
+ steps = 0
107
+
108
+ while steps < size:
109
+ parts.append(node_repr(current.value))
110
+ current = current.next
111
+ steps += 1
112
+
113
+ parts.append("(back to head)")
114
+ return " -> ".join(parts)
@@ -0,0 +1,126 @@
1
+ """High-level visualizer that dispatches to AsciiVisualizer based on list type."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .ascii_visualizer import AsciiVisualizer
7
+
8
+
9
+ class LinkedListVisualizer:
10
+ """Provides visualization and debug views for any pkstruct linked list."""
11
+
12
+ # Lazy imports to avoid circular dependencies at module load time.
13
+ @staticmethod
14
+ def _list_types() -> tuple[type, type, type]:
15
+ from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
16
+ from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
17
+ from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
18
+ return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
19
+
20
+ def visualize(self, list_instance: Any, style: str = "ascii") -> str:
21
+ """Return a string representation of *list_instance*.
22
+
23
+ Args:
24
+ list_instance: A SinglyLinkedList, DoublyLinkedList, or
25
+ CircularLinkedList instance.
26
+ style: Rendering style. Currently only ``"ascii"`` is supported.
27
+
28
+ Returns:
29
+ ASCII art string describing the list.
30
+
31
+ Raises:
32
+ TypeError: If *list_instance* is not a recognised list type.
33
+ ValueError: If *style* is not ``"ascii"``.
34
+ """
35
+ if style != "ascii":
36
+ raise ValueError(f"Unsupported visualisation style: {style!r}. Use 'ascii'.")
37
+
38
+ SinglyLinkedList, DoublyLinkedList, CircularLinkedList = self._list_types()
39
+
40
+ if isinstance(list_instance, CircularLinkedList):
41
+ return AsciiVisualizer.circular(
42
+ list_instance.head,
43
+ list_instance.size,
44
+ )
45
+ if isinstance(list_instance, DoublyLinkedList):
46
+ return AsciiVisualizer.doubly(
47
+ list_instance.head,
48
+ list_instance.tail,
49
+ )
50
+ if isinstance(list_instance, SinglyLinkedList):
51
+ return AsciiVisualizer.singly(list_instance.head)
52
+
53
+ raise TypeError(
54
+ f"Unsupported list type: {type(list_instance).__name__}. "
55
+ "Expected SinglyLinkedList, DoublyLinkedList, or CircularLinkedList."
56
+ )
57
+
58
+ def debug_view(self, list_instance: Any) -> dict[str, Any]:
59
+ """Return a diagnostic dictionary for *list_instance*.
60
+
61
+ The returned dict always contains:
62
+ - ``type``: class name string
63
+ - ``size``: integer node count
64
+ - ``head``: value of head node, or ``None``
65
+ - ``tail``: value of tail node, or ``None``
66
+ - ``values``: list of all values in forward order
67
+ - ``node_ids``: list of ``id()`` for each node
68
+
69
+ Args:
70
+ list_instance: Any supported linked list instance.
71
+
72
+ Returns:
73
+ Dictionary with debugging information.
74
+
75
+ Raises:
76
+ TypeError: If *list_instance* is not a recognised list type.
77
+ """
78
+ SinglyLinkedList, DoublyLinkedList, CircularLinkedList = self._list_types()
79
+
80
+ if not isinstance(list_instance, (SinglyLinkedList, DoublyLinkedList, CircularLinkedList)):
81
+ raise TypeError(
82
+ f"Unsupported list type: {type(list_instance).__name__}."
83
+ )
84
+
85
+ list_type = type(list_instance).__name__
86
+ size = list_instance.size
87
+
88
+ # Collect values and node ids safely
89
+ values: list[Any] = []
90
+ node_ids: list[int] = []
91
+
92
+ if isinstance(list_instance, CircularLinkedList):
93
+ current = list_instance.head
94
+ steps = 0
95
+ while current is not None and steps < size:
96
+ values.append(current.value)
97
+ node_ids.append(id(current))
98
+ current = current.next
99
+ steps += 1
100
+ else:
101
+ current = list_instance.head
102
+ visited: set[int] = set()
103
+ while current is not None:
104
+ if id(current) in visited:
105
+ break
106
+ visited.add(id(current))
107
+ values.append(current.value)
108
+ node_ids.append(id(current))
109
+ current = current.next
110
+
111
+ head_value = list_instance.head.value if list_instance.head is not None else None
112
+
113
+ # Tail: DoublyLinkedList has .tail; others we derive
114
+ if isinstance(list_instance, (DoublyLinkedList, CircularLinkedList)):
115
+ tail_value = list_instance.tail.value if list_instance.tail is not None else None
116
+ else:
117
+ tail_value = values[-1] if values else None
118
+
119
+ return {
120
+ "type": list_type,
121
+ "size": size,
122
+ "head": head_value,
123
+ "tail": tail_value,
124
+ "values": values,
125
+ "node_ids": node_ids,
126
+ }
@@ -0,0 +1,67 @@
1
+ """
2
+ pkstruct.shared
3
+ ~~~~~~~~~~~~~~~
4
+ Reusable infrastructure layer shared across all pkstruct modules.
5
+
6
+ Sub-packages
7
+ ------------
8
+ exceptions — Base exception hierarchy
9
+ validators — Input validation helpers
10
+ serializers — Safe JSON serialization
11
+ threading — StructureLock (RLock wrapper)
12
+ debugging — DebugTracer for step-tracing
13
+ benchmarking — timeit / compare helpers
14
+ visualization— Visualizable protocol
15
+ """
16
+ from pkstruct.shared.benchmarking import compare, timeit
17
+ from pkstruct.shared.debugging import DebugTracer
18
+ from pkstruct.shared.exceptions import (
19
+ ConcurrencyError,
20
+ EmptyStructureError,
21
+ IndexOutOfRangeError,
22
+ InvalidRangeError,
23
+ PkstructError,
24
+ SerializationError,
25
+ ValidationError,
26
+ ValueNotFoundError,
27
+ )
28
+ from pkstruct.shared.serializers import deserialize_from_json, serialize_to_json
29
+ from pkstruct.shared.threading import StructureLock
30
+ from pkstruct.shared.validators import (
31
+ validate_index,
32
+ validate_non_empty_list,
33
+ validate_non_negative_int,
34
+ validate_positive_int,
35
+ validate_range,
36
+ )
37
+ from pkstruct.shared.visualization import Visualizable
38
+
39
+ __all__ = [
40
+ # exceptions
41
+ "PkstructError",
42
+ "ValidationError",
43
+ "IndexOutOfRangeError",
44
+ "ValueNotFoundError",
45
+ "EmptyStructureError",
46
+ "SerializationError",
47
+ "ConcurrencyError",
48
+ "InvalidRangeError",
49
+ # validators
50
+ "validate_index",
51
+ "validate_range",
52
+ "validate_non_empty_list",
53
+ "validate_positive_int",
54
+ "validate_non_negative_int",
55
+ # serializers
56
+ "serialize_to_json",
57
+ "deserialize_from_json",
58
+ # threading
59
+ "StructureLock",
60
+ # debugging
61
+ "DebugTracer",
62
+ # benchmarking
63
+ "timeit",
64
+ "compare",
65
+ # visualization
66
+ "Visualizable",
67
+ ]
@@ -0,0 +1,78 @@
1
+ """
2
+ pkstruct.shared.benchmarking
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Micro-benchmarking utilities for pkstruct structures.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import statistics
9
+ import time
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+
14
+ def timeit(
15
+ fn: Callable[..., Any],
16
+ *args: Any,
17
+ iterations: int = 1000,
18
+ **kwargs: Any,
19
+ ) -> dict[str, float]:
20
+ """Time *fn* over *iterations* calls and return statistics (in seconds).
21
+
22
+ Args:
23
+ fn: The callable to benchmark.
24
+ *args: Positional arguments forwarded to *fn*.
25
+ iterations: Number of repetitions.
26
+ **kwargs: Keyword arguments forwarded to *fn*.
27
+
28
+ Returns:
29
+ A dict with keys ``min``, ``max``, ``mean``, ``median``, ``stdev``,
30
+ ``total``, and ``iterations``.
31
+ """
32
+ times: list[float] = []
33
+ for _ in range(iterations):
34
+ t0 = time.perf_counter()
35
+ fn(*args, **kwargs)
36
+ times.append(time.perf_counter() - t0)
37
+
38
+ return {
39
+ "min": min(times),
40
+ "max": max(times),
41
+ "mean": statistics.mean(times),
42
+ "median": statistics.median(times),
43
+ "stdev": statistics.pstdev(times),
44
+ "total": sum(times),
45
+ "iterations": iterations,
46
+ }
47
+
48
+
49
+ def compare(
50
+ label_a: str,
51
+ fn_a: Callable[..., Any],
52
+ label_b: str,
53
+ fn_b: Callable[..., Any],
54
+ *,
55
+ iterations: int = 1000,
56
+ ) -> dict[str, Any]:
57
+ """Compare two callables head-to-head.
58
+
59
+ Args:
60
+ label_a: Name for the first callable.
61
+ fn_a: First callable (no-arg).
62
+ label_b: Name for the second callable.
63
+ fn_b: Second callable (no-arg).
64
+ iterations: Repetitions per callable.
65
+
66
+ Returns:
67
+ Dict with per-callable stats and a ``"winner"`` key.
68
+ """
69
+ stats_a = timeit(fn_a, iterations=iterations)
70
+ stats_b = timeit(fn_b, iterations=iterations)
71
+ winner = label_a if stats_a["median"] <= stats_b["median"] else label_b
72
+ return {
73
+ label_a: stats_a,
74
+ label_b: stats_b,
75
+ "winner": winner,
76
+ "speedup": max(stats_a["median"], stats_b["median"])
77
+ / max(min(stats_a["median"], stats_b["median"]), 1e-12),
78
+ }
@@ -0,0 +1,69 @@
1
+ """
2
+ pkstruct.shared.debugging
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Step-tracing and introspection helpers for pkstruct structures.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from typing import Any
10
+
11
+
12
+ class DebugTracer:
13
+ """Lightweight operation tracer for debugging data structure mutations.
14
+
15
+ Keeps a bounded in-memory trace log (FIFO). Disabled by default to avoid
16
+ any overhead in production paths.
17
+
18
+ Example::
19
+
20
+ tracer = DebugTracer(enabled=True, max_entries=50)
21
+ tracer.record("insert", position=0, value=42)
22
+ for entry in tracer.entries:
23
+ print(entry)
24
+ """
25
+
26
+ __slots__ = ("enabled", "max_entries", "_entries")
27
+
28
+ def __init__(self, *, enabled: bool = False, max_entries: int = 200) -> None:
29
+ self.enabled = enabled
30
+ self.max_entries = max_entries
31
+ self._entries: list[dict[str, Any]] = []
32
+
33
+ def record(self, operation: str, **kwargs: Any) -> None:
34
+ """Record a single operation event.
35
+
36
+ Args:
37
+ operation: Human-readable operation name (e.g. ``"insert"``).
38
+ **kwargs: Arbitrary key-value context (e.g. ``position=2, value=99``).
39
+ """
40
+ if not self.enabled:
41
+ return
42
+ entry: dict[str, Any] = {
43
+ "op": operation,
44
+ "ts": time.monotonic(),
45
+ **kwargs,
46
+ }
47
+ self._entries.append(entry)
48
+ if len(self._entries) > self.max_entries:
49
+ self._entries.pop(0)
50
+
51
+ @property
52
+ def entries(self) -> list[dict[str, Any]]:
53
+ """Return a shallow copy of the trace log."""
54
+ return list(self._entries)
55
+
56
+ def clear(self) -> None:
57
+ """Clear the trace log."""
58
+ self._entries.clear()
59
+
60
+ def summary(self) -> dict[str, int]:
61
+ """Return a count of each operation type."""
62
+ counts: dict[str, int] = {}
63
+ for e in self._entries:
64
+ counts[e["op"]] = counts.get(e["op"], 0) + 1
65
+ return counts
66
+
67
+ def get_events(self) -> list[dict[str, Any]]:
68
+ """Return all recorded events."""
69
+ return self._entries
@@ -0,0 +1,59 @@
1
+ """
2
+ pkstruct.shared.exceptions
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Base exception hierarchy shared across all pkstruct modules.
5
+ """
6
+ from __future__ import annotations
7
+
8
+
9
+ class PkstructError(Exception):
10
+ """Root exception for all pkstruct errors."""
11
+
12
+
13
+ class ValidationError(PkstructError):
14
+ """Raised when input validation fails."""
15
+
16
+
17
+ class IndexOutOfRangeError(PkstructError, IndexError):
18
+ """Raised when a positional index is out of valid range."""
19
+
20
+ def __init__(self, index: int, size: int) -> None:
21
+ super().__init__(f"Index {index} is out of range for list of size {size}.")
22
+ self.index = index
23
+ self.size = size
24
+
25
+
26
+ class ValueNotFoundError(PkstructError, ValueError):
27
+ """Raised when a value is not found in the structure."""
28
+
29
+ def __init__(self, value: object) -> None:
30
+ super().__init__(f"Value {value!r} not found in the structure.")
31
+ self.value = value
32
+
33
+
34
+ class EmptyStructureError(PkstructError):
35
+ """Raised when an operation requires a non-empty structure."""
36
+
37
+ def __init__(self, operation: str = "operation") -> None:
38
+ super().__init__(f"Cannot perform '{operation}' on an empty structure.")
39
+ self.operation = operation
40
+
41
+
42
+ class SerializationError(PkstructError):
43
+ """Raised when serialization or deserialization fails."""
44
+
45
+
46
+ class ConcurrencyError(PkstructError):
47
+ """Raised on threading/locking issues."""
48
+
49
+
50
+ class InvalidRangeError(PkstructError, ValueError):
51
+ """Raised when a range (start, end) is logically invalid."""
52
+
53
+ def __init__(self, start: int, end: int, size: int) -> None:
54
+ super().__init__(
55
+ f"Range ({start}, {end}) is invalid for structure of size {size}."
56
+ )
57
+ self.start = start
58
+ self.end = end
59
+ self.size = size