trrack 0.0.1a1__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.
trrack/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Branching tree-based provenance tracking for Python.
2
+
3
+ Pure model with no UI dependencies: a :class:`ProvenanceGraph` of full-state
4
+ snapshots, plus an optional :class:`Store` seam for persistence.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib.metadata import version
10
+
11
+ from .graph import Node, ProvenanceGraph
12
+ from .persist import JsonFileStore, Store
13
+
14
+ __all__ = ["JsonFileStore", "Node", "ProvenanceGraph", "Store"]
15
+ __version__ = version("trrack")
trrack/graph.py ADDED
@@ -0,0 +1,165 @@
1
+ """The provenance graph model: full-state snapshot nodes and navigation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import asdict, dataclass
7
+ from typing import TYPE_CHECKING
8
+
9
+ from .persist import read_json, write_json_atomic
10
+
11
+ if TYPE_CHECKING:
12
+ import os
13
+
14
+
15
+ @dataclass
16
+ class Node:
17
+ """A single full-state snapshot and its position in the graph."""
18
+
19
+ id: str
20
+ parent: str | None
21
+ children: list[str]
22
+ created_at: str
23
+ label: str
24
+ state: dict
25
+
26
+
27
+ class ProvenanceGraph:
28
+ """A branching graph of full-state snapshots.
29
+
30
+ Pure model: no widget or marimo imports. Timestamps are supplied by the
31
+ caller via ``now`` so the model performs no I/O and stays deterministic.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ root_state: dict,
37
+ *,
38
+ now: str,
39
+ root_id: str | None = None,
40
+ ) -> None:
41
+ """Create the graph with a single root holding ``root_state``."""
42
+ rid = root_id or uuid.uuid4().hex
43
+ root = Node(
44
+ id=rid,
45
+ parent=None,
46
+ children=[],
47
+ created_at=now,
48
+ label="initial",
49
+ state=dict(root_state),
50
+ )
51
+ self.nodes: dict[str, Node] = {rid: root}
52
+ self.root_id = rid
53
+ self.current_id = rid
54
+
55
+ @property
56
+ def current(self) -> Node:
57
+ """The node the graph is currently positioned at."""
58
+ return self.nodes[self.current_id]
59
+
60
+ @property
61
+ def root(self) -> Node:
62
+ """The root node the graph started from."""
63
+ return self.nodes[self.root_id]
64
+
65
+ @property
66
+ def is_at_root(self) -> bool:
67
+ """Whether the current node is the root (nothing to undo)."""
68
+ return self.current.parent is None
69
+
70
+ @property
71
+ def is_at_latest(self) -> bool:
72
+ """Whether the current node is a leaf (nothing to redo)."""
73
+ return not self.current.children
74
+
75
+ def commit(
76
+ self,
77
+ state: dict,
78
+ *,
79
+ now: str,
80
+ label: str | None = None,
81
+ node_id: str | None = None,
82
+ ) -> Node:
83
+ """Append ``state`` as a child of the current node and move there."""
84
+ nid = node_id or uuid.uuid4().hex
85
+ parent = self.current
86
+ resolved_label = (
87
+ label if label is not None else self._auto_label(parent.state, state)
88
+ )
89
+ node = Node(
90
+ id=nid,
91
+ parent=parent.id,
92
+ children=[],
93
+ created_at=now,
94
+ label=resolved_label,
95
+ state=dict(state),
96
+ )
97
+ self.nodes[nid] = node
98
+ parent.children.append(nid)
99
+ self.current_id = nid
100
+ return node
101
+
102
+ def undo(self) -> Node | None:
103
+ """Move to the parent of the current node, or ``None`` at the root."""
104
+ node = self.current
105
+ if node.parent is None:
106
+ return None
107
+ self.current_id = node.parent
108
+ return self.current
109
+
110
+ def redo(self) -> Node | None:
111
+ """Move to the newest child of the current node, or ``None`` at a leaf."""
112
+ node = self.current
113
+ if not node.children:
114
+ return None
115
+ self.current_id = node.children[-1] # newest child (append order)
116
+ return self.current
117
+
118
+ def go_to(self, node_id: str) -> Node:
119
+ """Jump to ``node_id``; raises ``KeyError`` if it is unknown."""
120
+ if node_id not in self.nodes:
121
+ raise KeyError(node_id)
122
+ self.current_id = node_id
123
+ return self.current
124
+
125
+ def relabel(self, text: str, node_id: str | None = None) -> None:
126
+ """Set the label of ``node_id`` (default: the current node) to ``text``."""
127
+ self.nodes[node_id or self.current_id].label = text
128
+
129
+ def to_dict(self) -> dict:
130
+ """Serialize the whole graph to plain JSON-compatible data."""
131
+ return {
132
+ "nodes": {nid: asdict(node) for nid, node in self.nodes.items()},
133
+ "root_id": self.root_id,
134
+ "current_id": self.current_id,
135
+ }
136
+
137
+ @classmethod
138
+ def from_dict(cls, data: dict) -> ProvenanceGraph:
139
+ """Reconstruct a graph from :meth:`to_dict` output."""
140
+ tree = cls.__new__(cls)
141
+ tree.nodes = {nid: Node(**nd) for nid, nd in data["nodes"].items()}
142
+ tree.root_id = data["root_id"]
143
+ tree.current_id = data["current_id"]
144
+ return tree
145
+
146
+ def save(self, path: str | os.PathLike[str]) -> None:
147
+ """Write the serialized graph to ``path`` atomically."""
148
+ write_json_atomic(path, self.to_dict())
149
+
150
+ @classmethod
151
+ def load(cls, path: str | os.PathLike[str]) -> ProvenanceGraph | None:
152
+ """Load a graph from ``path``, or return ``None`` if it is absent."""
153
+ data = read_json(path)
154
+ if data is None:
155
+ return None
156
+ return cls.from_dict(data)
157
+
158
+ @staticmethod
159
+ def _auto_label(old: dict, new: dict) -> str:
160
+ changes = []
161
+ for key in sorted(set(old) | set(new)):
162
+ before, after = old.get(key), new.get(key)
163
+ if before != after:
164
+ changes.append(f"{key}: {before} → {after}")
165
+ return ", ".join(changes) if changes else "(no change)"
trrack/persist.py ADDED
@@ -0,0 +1,74 @@
1
+ """JSON persistence for a full-snapshot ProvenanceGraph.
2
+
3
+ A ``Store`` is the single extension seam: anything with ``save(dict)`` and
4
+ ``load() -> dict | None`` can back persistence, so a future backend (a database,
5
+ browser storage, a remote service) is a drop-in without a plugin registry.
6
+ ``JsonFileStore`` is the only implementation today.
7
+
8
+ v1 serializes the entire graph on every save: cost is O(total nodes x snapshot
9
+ size). That is imperceptible at interactive scale (tens-hundreds of small-state
10
+ nodes) and only matters with very large graphs or large per-node state. The
11
+ scaling answer, when needed, is a diff-node model -- store patches between nodes
12
+ with periodic full-state checkpoints, so per-save cost tracks changed state
13
+ rather than total state. Out of scope here; tracked as a future option.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import pathlib
20
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
21
+
22
+ if TYPE_CHECKING:
23
+ import os
24
+
25
+
26
+ @runtime_checkable
27
+ class Store(Protocol):
28
+ """A persistence backend for a serialized provenance payload."""
29
+
30
+ def save(self, data: dict) -> None:
31
+ """Persist ``data``, replacing any previously saved payload."""
32
+ ...
33
+
34
+ def load(self) -> dict | None:
35
+ """Return the saved payload, or ``None`` if nothing is stored."""
36
+ ...
37
+
38
+
39
+ def write_json_atomic(path: str | os.PathLike[str], data: dict) -> None:
40
+ """Write ``data`` as JSON, replacing ``path`` atomically.
41
+
42
+ The write goes to a sibling temp file first and is then moved into place
43
+ with ``Path.replace`` so a crash mid-write can never leave a half-written,
44
+ unparsable file at ``path``.
45
+ """
46
+ path = pathlib.Path(path)
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ tmp = path.with_name(path.name + ".tmp")
49
+ tmp.write_text(json.dumps(data), encoding="utf-8")
50
+ tmp.replace(path)
51
+
52
+
53
+ def read_json(path: str | os.PathLike[str]) -> dict | None:
54
+ """Return the parsed JSON at ``path``, or ``None`` if it does not exist."""
55
+ path = pathlib.Path(path)
56
+ if not path.exists():
57
+ return None
58
+ return json.loads(path.read_text(encoding="utf-8"))
59
+
60
+
61
+ class JsonFileStore:
62
+ """A ``Store`` that round-trips the payload through one JSON file."""
63
+
64
+ def __init__(self, path: str | os.PathLike[str]) -> None:
65
+ """Back persistence with the JSON file at ``path``."""
66
+ self._path = path
67
+
68
+ def save(self, data: dict) -> None:
69
+ """Write ``data`` to the file, atomically replacing prior contents."""
70
+ write_json_atomic(self._path, data)
71
+
72
+ def load(self) -> dict | None:
73
+ """Return the file's parsed payload, or ``None`` if it is absent."""
74
+ return read_json(self._path)
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: trrack
3
+ Version: 0.0.1a1
4
+ Summary: Branching tree-based provenance tracking for Python.
5
+ Project-URL: Homepage, https://github.com/kirangadhave/trrackpy
6
+ Project-URL: Repository, https://github.com/kirangadhave/trrackpy
7
+ Author: Kiran Gadhave
8
+ License-Expression: BSD-3-Clause
9
+ Keywords: history,provenance,time-travel,trrack,undo
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # trrack
21
+
22
+ Branching tree-based provenance tracking for Python. Pure model, no UI deps.
@@ -0,0 +1,6 @@
1
+ trrack/__init__.py,sha256=VMJGW8zTL4AHNdars1qVsE6as0ZrwS_ntF7fVNQlZHI,460
2
+ trrack/graph.py,sha256=Hl2--xPmXRZRpyOVi6G2P8dTYi4s1PfdZQn_qSYtt7M,5124
3
+ trrack/persist.py,sha256=S6Dfg5Vd0dQu99cQJo-BloMk0sWdHYOldDeSGyl7418,2704
4
+ trrack-0.0.1a1.dist-info/METADATA,sha256=PKcxj6m7k_LxQv-YweoGX8B6qfAP-gS4_WB42Bop-Nc,843
5
+ trrack-0.0.1a1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ trrack-0.0.1a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any