docworkspace 0.2.2__tar.gz → 0.2.3__tar.gz

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 (28) hide show
  1. {docworkspace-0.2.2 → docworkspace-0.2.3}/PKG-INFO +2 -2
  2. {docworkspace-0.2.2 → docworkspace-0.2.3}/pyproject.toml +2 -2
  3. docworkspace-0.2.3/src/docworkspace/workspace/io.py +169 -0
  4. docworkspace-0.2.3/tests/test_workspace_io_absolute_paths.py +80 -0
  5. docworkspace-0.2.3/uv.lock +125 -0
  6. docworkspace-0.2.2/src/docworkspace/workspace/io.py +0 -101
  7. docworkspace-0.2.2/uv.lock +0 -125
  8. {docworkspace-0.2.2 → docworkspace-0.2.3}/.github/workflows/ci.yml +0 -0
  9. {docworkspace-0.2.2 → docworkspace-0.2.3}/.github/workflows/release.yml +0 -0
  10. {docworkspace-0.2.2 → docworkspace-0.2.3}/.gitignore +0 -0
  11. {docworkspace-0.2.2 → docworkspace-0.2.3}/PUBLISH.md +0 -0
  12. {docworkspace-0.2.2 → docworkspace-0.2.3}/README.md +0 -0
  13. {docworkspace-0.2.2 → docworkspace-0.2.3}/pytest.ini +0 -0
  14. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/__init__.py +0 -0
  15. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/node/__init__.py +0 -0
  16. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/node/core.py +0 -0
  17. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/node/io.py +0 -0
  18. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/workspace/__init__.py +0 -0
  19. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/workspace/analysis.py +0 -0
  20. {docworkspace-0.2.2 → docworkspace-0.2.3}/src/docworkspace/workspace/core.py +0 -0
  21. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/conftest.py +0 -0
  22. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_fastapi_integration.py +0 -0
  23. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_node.py +0 -0
  24. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_node_io.py +0 -0
  25. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_simple_operations.py +0 -0
  26. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_workspace.py +0 -0
  27. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_workspace_serialization_types.py +0 -0
  28. {docworkspace-0.2.2 → docworkspace-0.2.3}/tests/test_workspace_shim.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docworkspace
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A workspace library for managing Polars dataframes with parent-child relationships and lazy evaluation
5
5
  Requires-Python: >=3.14
6
- Requires-Dist: polars-text>=0.1.2
6
+ Requires-Dist: polars-text>=0.1.3
7
7
  Description-Content-Type: text/markdown
8
8
 
9
9
  # DocWorkspace
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "docworkspace"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "A workspace library for managing Polars dataframes with parent-child relationships and lazy evaluation"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
7
- dependencies = ["polars-text>=0.1.2"]
7
+ dependencies = ["polars-text>=0.1.3"]
8
8
 
9
9
  [build-system]
10
10
  requires = ["hatchling"]
@@ -0,0 +1,169 @@
1
+ """Workspace file persistence helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Dict, Iterable, Union
8
+
9
+ from polars_text import list_source_paths, replace_source_paths
10
+
11
+ if TYPE_CHECKING: # pragma: no cover
12
+ from .core import Workspace
13
+
14
+ from ..node.io import NODE_DATA_DIR
15
+ from ..node.io import from_dict as node_from_dict
16
+ from ..node.io import to_dict as node_to_dict
17
+
18
+
19
+ def _resolve_metadata_path(path: Path) -> Path:
20
+ if path.is_dir():
21
+ return path / "metadata.json"
22
+ if path.suffix.lower() == ".json":
23
+ return path
24
+ raise ValueError("Workspace path must be a directory or a .json file")
25
+
26
+
27
+ def _collect_referenced_sources(plbin_paths: Iterable[Path]) -> set[Path]:
28
+ """Return the set of absolute source paths referenced by every plbin plan."""
29
+
30
+ referenced: set[Path] = set()
31
+ for plbin in plbin_paths:
32
+ for raw in list_source_paths(plbin):
33
+ referenced.add(Path(raw).resolve())
34
+ return referenced
35
+
36
+
37
+ def _rebase_plan_sources(plbin_path: Path, data_dir: Path) -> None:
38
+ """Rewrite scan source paths inside ``plbin_path`` to ``data_dir`` by basename.
39
+
40
+ Only paths whose basename exists under ``data_dir`` and whose stored value
41
+ differs from the resolved target are rewritten. Paths that already match
42
+ the current workspace layout are left untouched.
43
+ """
44
+
45
+ current_sources = list_source_paths(plbin_path)
46
+ if not current_sources:
47
+ return
48
+
49
+ mapping: dict[str, str] = {}
50
+ for old in current_sources:
51
+ target = (data_dir / Path(old).name).resolve()
52
+ if not target.exists():
53
+ continue
54
+ target_str = str(target)
55
+ if old == target_str:
56
+ continue
57
+ mapping[old] = target_str
58
+
59
+ if mapping:
60
+ replace_source_paths(plbin_path, mapping)
61
+
62
+
63
+ def _garbage_collect_workspace_data(
64
+ ws_root_dir: Path, nodes_data: list[dict[str, Any]]
65
+ ) -> None:
66
+ """Remove unreferenced parquet and plbin files from the workspace data dir.
67
+
68
+ * Parquet files not referenced by any registered node's plan are deleted.
69
+ * Plbin files whose name does not match a registered node's ``data_path``
70
+ are deleted (they belong to nodes no longer in the workspace).
71
+ """
72
+
73
+ data_dir = ws_root_dir / NODE_DATA_DIR
74
+ if not data_dir.exists() or not data_dir.is_dir():
75
+ return
76
+
77
+ expected_plbin_names = {
78
+ Path(str(node_payload["data_path"])).name for node_payload in nodes_data
79
+ }
80
+ registered_plbins = [
81
+ data_dir / name for name in expected_plbin_names if (data_dir / name).exists()
82
+ ]
83
+
84
+ referenced_sources = _collect_referenced_sources(registered_plbins)
85
+
86
+ for candidate in data_dir.iterdir():
87
+ if not candidate.is_file():
88
+ continue
89
+ suffix = candidate.suffix.lower()
90
+ if suffix == ".plbin":
91
+ if candidate.name not in expected_plbin_names:
92
+ candidate.unlink(missing_ok=True)
93
+ elif suffix == ".parquet":
94
+ if candidate.resolve() not in referenced_sources:
95
+ candidate.unlink(missing_ok=True)
96
+
97
+
98
+ def write_workspace(workspace: "Workspace", path: Union[str, Path]) -> None:
99
+ target = _resolve_metadata_path(Path(path))
100
+ target.parent.mkdir(parents=True, exist_ok=True)
101
+ workspace.ws_root_dir = target.parent
102
+
103
+ description = workspace.description
104
+ created_at = workspace.created_at
105
+ modified_at = workspace.modified_at
106
+
107
+ nodes_data: list[dict[str, Any]] = []
108
+ for node in workspace.nodes.values():
109
+ nodes_data.append(node_to_dict(node))
110
+
111
+ workspace_metadata: Dict[str, Any] = {
112
+ "id": workspace.id,
113
+ "name": workspace.name,
114
+ "version": 2,
115
+ "description": description,
116
+ "created_at": created_at,
117
+ "modified_at": modified_at,
118
+ }
119
+
120
+ data = {"workspace_metadata": workspace_metadata, "nodes": nodes_data}
121
+ with target.open("w", encoding="utf-8") as f:
122
+ json.dump(data, f, ensure_ascii=False, indent=2)
123
+
124
+ _garbage_collect_workspace_data(target.parent, nodes_data)
125
+
126
+
127
+ def read_workspace_metadata(path: Union[str, Path]) -> Dict[str, Any]:
128
+ """Load and return the workspace metadata dictionary from metadata.json.
129
+
130
+ This helper only reads/parses the JSON metadata file and does not attempt
131
+ to load any node data payload files.
132
+ """
133
+
134
+ target = _resolve_metadata_path(Path(path))
135
+ with target.open("r", encoding="utf-8") as f:
136
+ return json.load(f)
137
+
138
+
139
+ def read_workspace(path: Union[str, Path]) -> "Workspace":
140
+ from .core import Workspace
141
+
142
+ target = _resolve_metadata_path(Path(path))
143
+ data = read_workspace_metadata(path)
144
+
145
+ ws_meta = data.get("workspace_metadata", {})
146
+ workspace = Workspace(
147
+ name=ws_meta.get("name", "restored_workspace"),
148
+ ws_root_dir=target.parent,
149
+ )
150
+ workspace.id = ws_meta.get("id", workspace.id)
151
+ workspace.description = ws_meta.get("description", "") or ""
152
+ workspace.created_at = ws_meta.get("created_at")
153
+ workspace.modified_at = ws_meta.get("modified_at")
154
+
155
+ data_dir = (target.parent / NODE_DATA_DIR).resolve()
156
+ for node_entry in data.get("nodes", []):
157
+ plbin_abs = (target.parent / Path(str(node_entry["data_path"]))).resolve()
158
+ if plbin_abs.exists():
159
+ _rebase_plan_sources(plbin_abs, data_dir)
160
+ workspace.add_node(node_from_dict(node_entry, workspace=workspace))
161
+
162
+ return workspace
163
+
164
+
165
+ __all__ = [
166
+ "write_workspace",
167
+ "read_workspace_metadata",
168
+ "read_workspace",
169
+ ]
@@ -0,0 +1,80 @@
1
+ """Workspace save-time garbage collection and load-time path rebasing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import cast
8
+
9
+ import polars as pl
10
+ from polars_text import list_source_paths
11
+
12
+ from docworkspace.node import Node
13
+ from docworkspace.workspace import Workspace
14
+
15
+
16
+ def _make_parquet(path: Path, df: pl.DataFrame) -> None:
17
+ path.parent.mkdir(parents=True, exist_ok=True)
18
+ df.write_parquet(path)
19
+
20
+
21
+ def test_save_removes_orphan_parquet_and_plbin(tmp_path: Path):
22
+ ws_root = tmp_path / "ws"
23
+ ws_root.mkdir()
24
+ data_dir = ws_root / "data"
25
+ data_dir.mkdir()
26
+
27
+ kept_parquet = data_dir / "kept.parquet"
28
+ _make_parquet(kept_parquet, pl.DataFrame({"a": [1, 2, 3]}))
29
+
30
+ orphan_parquet = data_dir / "orphan.parquet"
31
+ _make_parquet(orphan_parquet, pl.DataFrame({"b": [9]}))
32
+
33
+ orphan_plbin = data_dir / "ghost-node-id.plbin"
34
+ orphan_plbin.write_bytes(b"not-a-real-plan")
35
+
36
+ ws = Workspace(name="gc_ws", ws_root_dir=ws_root)
37
+ lazy = pl.scan_parquet(kept_parquet.resolve())
38
+ ws.add_node(Node(data=lazy, name="kept"))
39
+
40
+ ws.save(ws_root)
41
+
42
+ assert kept_parquet.exists(), "Referenced parquet must be kept"
43
+ assert not orphan_parquet.exists(), "Unreferenced parquet must be deleted"
44
+ assert not orphan_plbin.exists(), "Unregistered plbin must be deleted"
45
+
46
+ plbin_files = list(data_dir.glob("*.plbin"))
47
+ assert len(plbin_files) == 1
48
+ sources = list_source_paths(plbin_files[0])
49
+ assert any(Path(s) == kept_parquet.resolve() for s in sources)
50
+
51
+
52
+ def test_load_rebases_source_paths_after_workspace_move(tmp_path: Path):
53
+ original_root = tmp_path / "original"
54
+ original_root.mkdir()
55
+ data_dir = original_root / "data"
56
+ data_dir.mkdir()
57
+
58
+ parquet_path = data_dir / "input.parquet"
59
+ _make_parquet(parquet_path, pl.DataFrame({"x": [10, 20, 30]}))
60
+
61
+ ws = Workspace(name="move_ws", ws_root_dir=original_root)
62
+ lazy = pl.scan_parquet(parquet_path.resolve())
63
+ ws.add_node(Node(data=lazy, name="input"))
64
+ ws.save(original_root)
65
+
66
+ moved_root = tmp_path / "moved"
67
+ shutil.copytree(original_root, moved_root)
68
+ shutil.rmtree(original_root)
69
+
70
+ ws_reloaded = Workspace.load(moved_root)
71
+ assert len(ws_reloaded.nodes) == 1
72
+ node = next(iter(ws_reloaded.nodes.values()))
73
+
74
+ collected = cast(pl.DataFrame, node.data.collect())
75
+ assert collected.to_series().to_list() == [10, 20, 30]
76
+
77
+ plbin_files = list((moved_root / "data").glob("*.plbin"))
78
+ sources = list_source_paths(plbin_files[0])
79
+ moved_parquet = (moved_root / "data" / "input.parquet").resolve()
80
+ assert all(Path(s) == moved_parquet for s in sources)
@@ -0,0 +1,125 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.14"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "docworkspace"
16
+ version = "0.2.3"
17
+ source = { editable = "." }
18
+ dependencies = [
19
+ { name = "polars-text" },
20
+ ]
21
+
22
+ [package.dev-dependencies]
23
+ dev = [
24
+ { name = "pytest" },
25
+ ]
26
+
27
+ [package.metadata]
28
+ requires-dist = [{ name = "polars-text", specifier = ">=0.1.3" }]
29
+
30
+ [package.metadata.requires-dev]
31
+ dev = [{ name = "pytest", specifier = ">=8.0.0" }]
32
+
33
+ [[package]]
34
+ name = "iniconfig"
35
+ version = "2.3.0"
36
+ source = { registry = "https://pypi.org/simple" }
37
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
38
+ wheels = [
39
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
40
+ ]
41
+
42
+ [[package]]
43
+ name = "packaging"
44
+ version = "26.1"
45
+ source = { registry = "https://pypi.org/simple" }
46
+ sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
47
+ wheels = [
48
+ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
49
+ ]
50
+
51
+ [[package]]
52
+ name = "pluggy"
53
+ version = "1.6.0"
54
+ source = { registry = "https://pypi.org/simple" }
55
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "polars"
62
+ version = "1.40.0"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ dependencies = [
65
+ { name = "polars-runtime-32" },
66
+ ]
67
+ sdist = { url = "https://files.pythonhosted.org/packages/d9/1b/eea7d6fe6daafc1d784cc0f76c729b28051837ccb2d51ae64a0a3f798142/polars-1.40.0.tar.gz", hash = "sha256:711dd50dcbc35ba42a2625fcadc2a1349e2e9abf48e35631bdabafb90d89874b", size = 732943, upload-time = "2026-04-18T05:25:26.077Z" }
68
+ wheels = [
69
+ { url = "https://files.pythonhosted.org/packages/b4/ad/d5ed79269b7fe59a3dbbfbdbecbe1e59a0b56e38d36491e57d2bfb5846c1/polars-1.40.0-py3-none-any.whl", hash = "sha256:60b1d677ca363e2fc6fdea8c3d16c0653fd52cc37f0249e0f29d9536d5aa45ef", size = 828012, upload-time = "2026-04-18T05:23:39.055Z" },
70
+ ]
71
+
72
+ [[package]]
73
+ name = "polars-runtime-32"
74
+ version = "1.40.0"
75
+ source = { registry = "https://pypi.org/simple" }
76
+ sdist = { url = "https://files.pythonhosted.org/packages/fb/b2/eae6c1b3d16c7a64ff382f557985ff939cce13455e8c9d056ab8e1e0fc87/polars_runtime_32-1.40.0.tar.gz", hash = "sha256:e31bff8bd37492c714e155e2e1429ac2d9ddf2dd6ec6474cc1cc70ac0b2bd6af", size = 2935285, upload-time = "2026-04-18T05:25:28.038Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/b0/e4/2325689d2af4f9e70699ff98e8a2543707bebc34af78a5fe0e654107d9ed/polars_runtime_32-1.40.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:cab3ac7ff5bc9e0f4b3b146015569e9417cf0eaff8d3fb71004d73d67b6f09c7", size = 52092528, upload-time = "2026-04-18T05:23:42.341Z" },
79
+ { url = "https://files.pythonhosted.org/packages/19/a6/82157b19c5c40b2c1ed0493b87b9eaf9b4863cdedca5575ee083488b45ba/polars_runtime_32-1.40.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d29624c75c4049253300786d00882fce620b3677ce495ebc4199292de8c2ba02", size = 46365073, upload-time = "2026-04-18T05:23:46.7Z" },
80
+ { url = "https://files.pythonhosted.org/packages/85/b5/5c4f1f2545f56c664cc57bbdd1aa66fcfcb129aa137ed72cc81d58eb480f/polars_runtime_32-1.40.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a034dc0d8481fc1ca0456ab33e98e53a4c6d6cc6a2edb36246cc81c936b925dc", size = 50250561, upload-time = "2026-04-18T05:23:51.316Z" },
81
+ { url = "https://files.pythonhosted.org/packages/8e/51/cb5eb75394f39c0ec14fddcc9b11adb707e1f28224a552ecbfa72d39b61b/polars_runtime_32-1.40.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70e78c2f13a54a9d92ae30d2625bda759173cc4867ad6a39f85f140058d899c6", size = 56243695, upload-time = "2026-04-18T05:23:55.932Z" },
82
+ { url = "https://files.pythonhosted.org/packages/16/3a/be1437c0fbecbb07d81b151456089c3cf054eea5a791f849ed39b67611ca/polars_runtime_32-1.40.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1843272c0ef49f4a07435888f0059eca08ec16ab9880219c457195a081df0281", size = 50427843, upload-time = "2026-04-18T05:24:00.159Z" },
83
+ { url = "https://files.pythonhosted.org/packages/be/c7/ea6449a2161816a13ed1d8aa02177d5a0594e011f0df5ddd2fad8e5bf20e/polars_runtime_32-1.40.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:081237dba07f15d61fc151825f203165480e9503ebe72a474a8c99aa78021962", size = 54153077, upload-time = "2026-04-18T05:24:05.066Z" },
84
+ { url = "https://files.pythonhosted.org/packages/aa/1a/0b239138afe8b80a1a0b4c95db3884e6afbbe82ec3318918ab03bc57f231/polars_runtime_32-1.40.0-cp310-abi3-win_amd64.whl", hash = "sha256:a916040e0b7f461ce987e4551fed9eea5914b4fbb5af907b1d9e80db71fadeb5", size = 51822748, upload-time = "2026-04-18T05:24:09.384Z" },
85
+ { url = "https://files.pythonhosted.org/packages/06/ce/c16ef8fd3030b7342032b040fab21a42f6fee57e47ee7f41e2f1a1e36f01/polars_runtime_32-1.40.0-cp310-abi3-win_arm64.whl", hash = "sha256:719c64eecde24a95aa3599eb9c8efc98c1499bab7ef9c01cbbe8939cd583e654", size = 45819617, upload-time = "2026-04-18T05:24:13.214Z" },
86
+ ]
87
+
88
+ [[package]]
89
+ name = "polars-text"
90
+ version = "0.1.3"
91
+ source = { registry = "https://pypi.org/simple" }
92
+ dependencies = [
93
+ { name = "polars" },
94
+ ]
95
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/36/03bb22e59897e878b9b73cf7f8bb826b587affd42276591619a14b955483/polars_text-0.1.3.tar.gz", hash = "sha256:06b361a607d7a6fa204b040768f159acad88c7f6daf64a213a776f10b7468d92", size = 5357581, upload-time = "2026-04-21T14:03:28.096Z" }
96
+ wheels = [
97
+ { url = "https://files.pythonhosted.org/packages/5c/54/ec8bb8005b4b2fc70787b6191751d92d2eff3a8c7aa34964ec0aca145451/polars_text-0.1.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e3dad2a26f5d18b5eca8075b97103ad50490a95d735a3d83ddbf805d92f40b4f", size = 16551094, upload-time = "2026-04-21T14:03:18.03Z" },
98
+ { url = "https://files.pythonhosted.org/packages/a8/da/363f0d519bc0df83e71f2815f8c7018ee4c50876f1f23a2952e1e2739930/polars_text-0.1.3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:245d3703cce24f5e9afea9ba746eb991c3cc6b06a5a7c5fd6863a22f42172331", size = 20896342, upload-time = "2026-04-21T14:03:21.747Z" },
99
+ { url = "https://files.pythonhosted.org/packages/ed/4f/9de5690509cea46661e9398290a037a472f43eb935a378d158feeddf4792/polars_text-0.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:0e3f6084581756925867b1c393ab4b6407d4eef05ea7cf46d9c5c4b25b925a74", size = 16329073, upload-time = "2026-04-21T14:03:25.411Z" },
100
+ ]
101
+
102
+ [[package]]
103
+ name = "pygments"
104
+ version = "2.20.0"
105
+ source = { registry = "https://pypi.org/simple" }
106
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
107
+ wheels = [
108
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
109
+ ]
110
+
111
+ [[package]]
112
+ name = "pytest"
113
+ version = "9.0.3"
114
+ source = { registry = "https://pypi.org/simple" }
115
+ dependencies = [
116
+ { name = "colorama", marker = "sys_platform == 'win32'" },
117
+ { name = "iniconfig" },
118
+ { name = "packaging" },
119
+ { name = "pluggy" },
120
+ { name = "pygments" },
121
+ ]
122
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
123
+ wheels = [
124
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
125
+ ]
@@ -1,101 +0,0 @@
1
- """Workspace file persistence helpers."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from pathlib import Path
7
- from typing import TYPE_CHECKING, Any, Dict, Union
8
-
9
- if TYPE_CHECKING: # pragma: no cover
10
- from .core import Workspace
11
-
12
- from ..node.io import from_dict as node_from_dict
13
- from ..node.io import to_dict as node_to_dict
14
-
15
-
16
- def _resolve_metadata_path(path: Path) -> Path:
17
- if path.is_dir():
18
- return path / "metadata.json"
19
- if path.suffix.lower() == ".json":
20
- return path
21
- raise ValueError("Workspace path must be a directory or a .json file")
22
-
23
-
24
- def write_workspace(workspace: "Workspace", path: Union[str, Path]) -> None:
25
- target = _resolve_metadata_path(Path(path))
26
- target.parent.mkdir(parents=True, exist_ok=True)
27
- workspace.ws_root_dir = target.parent
28
-
29
- description = workspace.description
30
- created_at = workspace.created_at
31
- modified_at = workspace.modified_at
32
-
33
- nodes_data: list[dict[str, Any]] = []
34
- for node in workspace.nodes.values():
35
- nodes_data.append(node_to_dict(node))
36
-
37
- workspace_metadata: Dict[str, Any] = {
38
- "id": workspace.id,
39
- "name": workspace.name,
40
- "version": 2,
41
- "description": description,
42
- "created_at": created_at,
43
- "modified_at": modified_at,
44
- }
45
-
46
- data = {"workspace_metadata": workspace_metadata, "nodes": nodes_data}
47
- with target.open("w", encoding="utf-8") as f:
48
- json.dump(data, f, ensure_ascii=False, indent=2)
49
-
50
- try:
51
- data_dir = target.parent / "data"
52
- expected_node_files = {
53
- Path(str(node_payload["data_path"])).name for node_payload in nodes_data
54
- }
55
- if data_dir.exists() and data_dir.is_dir():
56
- for candidate in data_dir.glob("*.plbin"):
57
- if candidate.name not in expected_node_files:
58
- candidate.unlink(missing_ok=True)
59
- except Exception:
60
- pass
61
-
62
-
63
- def read_workspace_metadata(path: Union[str, Path]) -> Dict[str, Any]:
64
- """Load and return the workspace metadata dictionary from metadata.json.
65
-
66
- This helper only reads/parses the JSON metadata file and does not attempt
67
- to load any node data payload files.
68
- """
69
-
70
- target = _resolve_metadata_path(Path(path))
71
- with target.open("r", encoding="utf-8") as f:
72
- return json.load(f)
73
-
74
-
75
- def read_workspace(path: Union[str, Path]) -> "Workspace":
76
- from .core import Workspace
77
-
78
- target = _resolve_metadata_path(Path(path))
79
- data = read_workspace_metadata(path)
80
-
81
- ws_meta = data.get("workspace_metadata", {})
82
- workspace = Workspace(
83
- name=ws_meta.get("name", "restored_workspace"),
84
- ws_root_dir=target.parent,
85
- )
86
- workspace.id = ws_meta.get("id", workspace.id)
87
- workspace.description = ws_meta.get("description", "") or ""
88
- workspace.created_at = ws_meta.get("created_at")
89
- workspace.modified_at = ws_meta.get("modified_at")
90
-
91
- for node_entry in data.get("nodes", []):
92
- workspace.add_node(node_from_dict(node_entry, workspace=workspace))
93
-
94
- return workspace
95
-
96
-
97
- __all__ = [
98
- "write_workspace",
99
- "read_workspace_metadata",
100
- "read_workspace",
101
- ]
@@ -1,125 +0,0 @@
1
- version = 1
2
- revision = 3
3
- requires-python = ">=3.14"
4
-
5
- [[package]]
6
- name = "colorama"
7
- version = "0.4.6"
8
- source = { registry = "https://pypi.org/simple" }
9
- sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
- wheels = [
11
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
- ]
13
-
14
- [[package]]
15
- name = "docworkspace"
16
- version = "0.2.2"
17
- source = { editable = "." }
18
- dependencies = [
19
- { name = "polars-text" },
20
- ]
21
-
22
- [package.dev-dependencies]
23
- dev = [
24
- { name = "pytest" },
25
- ]
26
-
27
- [package.metadata]
28
- requires-dist = [{ name = "polars-text", specifier = ">=0.1.2" }]
29
-
30
- [package.metadata.requires-dev]
31
- dev = [{ name = "pytest", specifier = ">=8.0.0" }]
32
-
33
- [[package]]
34
- name = "iniconfig"
35
- version = "2.3.0"
36
- source = { registry = "https://pypi.org/simple" }
37
- sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
38
- wheels = [
39
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
40
- ]
41
-
42
- [[package]]
43
- name = "packaging"
44
- version = "26.0"
45
- source = { registry = "https://pypi.org/simple" }
46
- sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
47
- wheels = [
48
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
49
- ]
50
-
51
- [[package]]
52
- name = "pluggy"
53
- version = "1.6.0"
54
- source = { registry = "https://pypi.org/simple" }
55
- sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
56
- wheels = [
57
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
58
- ]
59
-
60
- [[package]]
61
- name = "polars"
62
- version = "1.39.3"
63
- source = { registry = "https://pypi.org/simple" }
64
- dependencies = [
65
- { name = "polars-runtime-32" },
66
- ]
67
- sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" }
68
- wheels = [
69
- { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" },
70
- ]
71
-
72
- [[package]]
73
- name = "polars-runtime-32"
74
- version = "1.39.3"
75
- source = { registry = "https://pypi.org/simple" }
76
- sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" }
77
- wheels = [
78
- { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" },
79
- { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" },
80
- { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" },
81
- { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" },
82
- { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" },
83
- { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" },
84
- { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" },
85
- { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" },
86
- ]
87
-
88
- [[package]]
89
- name = "polars-text"
90
- version = "0.1.2"
91
- source = { registry = "https://pypi.org/simple" }
92
- dependencies = [
93
- { name = "polars" },
94
- ]
95
- sdist = { url = "https://files.pythonhosted.org/packages/42/b9/1653c8ac742e3b84ebe328d213b5eb09ee96ce6241c27364911a2e29e2aa/polars_text-0.1.2.tar.gz", hash = "sha256:2a080c8e7e0ecef10b3b431fb14dbb90de6faf552e24ffa30f52f0876c5fe5e8", size = 53365, upload-time = "2026-04-07T23:41:59.225Z" }
96
- wheels = [
97
- { url = "https://files.pythonhosted.org/packages/b4/60/829e2048d45aa63ec9c2634f63bf750b9569aded969920db3240737ea9e9/polars_text-0.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7dbd56bc3c90d87defc9e682e816ff7cdf264f19ee458df190322ba92c10c20", size = 7659172, upload-time = "2026-04-07T23:41:53.444Z" },
98
- { url = "https://files.pythonhosted.org/packages/a5/60/6417a5e73915e0dbc4c1f0643b41c715428a5350f4b2a97903655b526342/polars_text-0.1.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0283306eb5c185cac89db3abf1ecf877b47c4e18a2afc4ad8892a5ab12116b8f", size = 10981911, upload-time = "2026-04-07T23:41:55.388Z" },
99
- { url = "https://files.pythonhosted.org/packages/da/a6/ff1dd58b29c7ac03e7344311e2b82f211d043321d8f9705a9e8e664db900/polars_text-0.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:5b80da5e9902ecaefc4db227a38d20fc6c328ee4254678319329b38248c2ea4e", size = 7771979, upload-time = "2026-04-07T23:41:57.646Z" },
100
- ]
101
-
102
- [[package]]
103
- name = "pygments"
104
- version = "2.20.0"
105
- source = { registry = "https://pypi.org/simple" }
106
- sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
107
- wheels = [
108
- { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
109
- ]
110
-
111
- [[package]]
112
- name = "pytest"
113
- version = "9.0.3"
114
- source = { registry = "https://pypi.org/simple" }
115
- dependencies = [
116
- { name = "colorama", marker = "sys_platform == 'win32'" },
117
- { name = "iniconfig" },
118
- { name = "packaging" },
119
- { name = "pluggy" },
120
- { name = "pygments" },
121
- ]
122
- sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
123
- wheels = [
124
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
125
- ]
File without changes
File without changes
File without changes
File without changes