docworkspace 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +2 -2
  2. {docworkspace-0.2.2 → docworkspace-0.2.4}/pyproject.toml +2 -2
  3. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/workspace/__init__.py +2 -0
  4. docworkspace-0.2.4/src/docworkspace/workspace/io.py +185 -0
  5. docworkspace-0.2.4/tests/test_workspace_io_absolute_paths.py +128 -0
  6. docworkspace-0.2.4/uv.lock +125 -0
  7. docworkspace-0.2.2/src/docworkspace/workspace/io.py +0 -101
  8. docworkspace-0.2.2/uv.lock +0 -125
  9. {docworkspace-0.2.2 → docworkspace-0.2.4}/.github/workflows/ci.yml +0 -0
  10. {docworkspace-0.2.2 → docworkspace-0.2.4}/.github/workflows/release.yml +0 -0
  11. {docworkspace-0.2.2 → docworkspace-0.2.4}/.gitignore +0 -0
  12. {docworkspace-0.2.2 → docworkspace-0.2.4}/PUBLISH.md +0 -0
  13. {docworkspace-0.2.2 → docworkspace-0.2.4}/README.md +0 -0
  14. {docworkspace-0.2.2 → docworkspace-0.2.4}/pytest.ini +0 -0
  15. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/__init__.py +0 -0
  16. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/node/__init__.py +0 -0
  17. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/node/core.py +0 -0
  18. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/node/io.py +0 -0
  19. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/workspace/analysis.py +0 -0
  20. {docworkspace-0.2.2 → docworkspace-0.2.4}/src/docworkspace/workspace/core.py +0 -0
  21. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/conftest.py +0 -0
  22. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_fastapi_integration.py +0 -0
  23. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_node.py +0 -0
  24. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_node_io.py +0 -0
  25. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_simple_operations.py +0 -0
  26. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_workspace.py +0 -0
  27. {docworkspace-0.2.2 → docworkspace-0.2.4}/tests/test_workspace_serialization_types.py +0 -0
  28. {docworkspace-0.2.2 → docworkspace-0.2.4}/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.4
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.4"
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"]
@@ -8,9 +8,11 @@ analysis and graph views so internal relative imports
8
8
  from .analysis import graph_json # noqa: F401
9
9
  from .analysis import info_json # noqa: F401
10
10
  from .core import Workspace # noqa: F401
11
+ from .io import rebase_workspace_sources # noqa: F401
11
12
 
12
13
  __all__ = [
13
14
  "Workspace",
14
15
  "info_json",
15
16
  "graph_json",
17
+ "rebase_workspace_sources",
16
18
  ]
@@ -0,0 +1,185 @@
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
+ for node_entry in data.get("nodes", []):
156
+ workspace.add_node(node_from_dict(node_entry, workspace=workspace))
157
+
158
+ return workspace
159
+
160
+
161
+ def rebase_workspace_sources(path: Union[str, Path]) -> None:
162
+ """Rewrite stale scan source paths inside every plbin listed in ``metadata.json``.
163
+
164
+ Call this **after** the workspace folder has reached its final location on
165
+ disk (i.e. after any rename / move) but **before** deserializing the
166
+ workspace nodes into memory, so that ``LazyFrame.deserialize`` sees paths
167
+ that match the current filesystem.
168
+ """
169
+
170
+ target = _resolve_metadata_path(Path(path))
171
+ data = read_workspace_metadata(path)
172
+ data_dir = (target.parent / NODE_DATA_DIR).resolve()
173
+
174
+ for node_entry in data.get("nodes", []):
175
+ plbin_abs = (target.parent / Path(str(node_entry["data_path"]))).resolve()
176
+ if plbin_abs.exists():
177
+ _rebase_plan_sources(plbin_abs, data_dir)
178
+
179
+
180
+ __all__ = [
181
+ "write_workspace",
182
+ "read_workspace_metadata",
183
+ "read_workspace",
184
+ "rebase_workspace_sources",
185
+ ]
@@ -0,0 +1,128 @@
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
+ from docworkspace.workspace.io import rebase_workspace_sources
15
+
16
+
17
+ def _make_parquet(path: Path, df: pl.DataFrame) -> None:
18
+ path.parent.mkdir(parents=True, exist_ok=True)
19
+ df.write_parquet(path)
20
+
21
+
22
+ def test_save_removes_orphan_parquet_and_plbin(tmp_path: Path):
23
+ ws_root = tmp_path / "ws"
24
+ ws_root.mkdir()
25
+ data_dir = ws_root / "data"
26
+ data_dir.mkdir()
27
+
28
+ kept_parquet = data_dir / "kept.parquet"
29
+ _make_parquet(kept_parquet, pl.DataFrame({"a": [1, 2, 3]}))
30
+
31
+ orphan_parquet = data_dir / "orphan.parquet"
32
+ _make_parquet(orphan_parquet, pl.DataFrame({"b": [9]}))
33
+
34
+ orphan_plbin = data_dir / "ghost-node-id.plbin"
35
+ orphan_plbin.write_bytes(b"not-a-real-plan")
36
+
37
+ ws = Workspace(name="gc_ws", ws_root_dir=ws_root)
38
+ lazy = pl.scan_parquet(kept_parquet.resolve())
39
+ ws.add_node(Node(data=lazy, name="kept"))
40
+
41
+ ws.save(ws_root)
42
+
43
+ assert kept_parquet.exists(), "Referenced parquet must be kept"
44
+ assert not orphan_parquet.exists(), "Unreferenced parquet must be deleted"
45
+ assert not orphan_plbin.exists(), "Unregistered plbin must be deleted"
46
+
47
+ plbin_files = list(data_dir.glob("*.plbin"))
48
+ assert len(plbin_files) == 1
49
+ sources = list_source_paths(plbin_files[0])
50
+ assert any(Path(s) == kept_parquet.resolve() for s in sources)
51
+
52
+
53
+ def test_rebase_then_load_after_workspace_move(tmp_path: Path):
54
+ """Simulate the real backend flow: rebase plbin files, then load workspace."""
55
+
56
+ original_root = tmp_path / "original"
57
+ original_root.mkdir()
58
+ data_dir = original_root / "data"
59
+ data_dir.mkdir()
60
+
61
+ parquet_path = data_dir / "input.parquet"
62
+ _make_parquet(parquet_path, pl.DataFrame({"x": [10, 20, 30]}))
63
+
64
+ ws = Workspace(name="move_ws", ws_root_dir=original_root)
65
+ lazy = pl.scan_parquet(parquet_path.resolve())
66
+ ws.add_node(Node(data=lazy, name="input"))
67
+ ws.save(original_root)
68
+
69
+ moved_root = tmp_path / "moved"
70
+ shutil.copytree(original_root, moved_root)
71
+ shutil.rmtree(original_root)
72
+
73
+ # Rebase on disk BEFORE loading (mirrors backend set_current_workspace).
74
+ rebase_workspace_sources(moved_root)
75
+
76
+ ws_reloaded = Workspace.load(moved_root)
77
+ assert len(ws_reloaded.nodes) == 1
78
+ node = next(iter(ws_reloaded.nodes.values()))
79
+
80
+ collected = cast(pl.DataFrame, node.data.collect())
81
+ assert collected.to_series().to_list() == [10, 20, 30]
82
+
83
+ plbin_files = list((moved_root / "data").glob("*.plbin"))
84
+ sources = list_source_paths(plbin_files[0])
85
+ moved_parquet = (moved_root / "data" / "input.parquet").resolve()
86
+ assert all(Path(s) == moved_parquet for s in sources)
87
+
88
+
89
+ def test_rebase_then_rename_then_save_keeps_parquet(tmp_path: Path):
90
+ """Repro for the reported bug: rename folder after save, then load + unload.
91
+
92
+ Sequence:
93
+ 1. Save workspace in folder A.
94
+ 2. Rename folder A → B (simulating user rename).
95
+ 3. rebase_workspace_sources(B) → plbin paths point at B/data/…
96
+ 4. Load workspace from B.
97
+ 5. Save workspace back to B.
98
+ 6. Parquet referenced by the node must survive GC.
99
+ """
100
+
101
+ folder_a = tmp_path / "Test"
102
+ folder_a.mkdir()
103
+ data_dir = folder_a / "data"
104
+ data_dir.mkdir()
105
+
106
+ parquet_path = data_dir / "my_data.parquet"
107
+ _make_parquet(parquet_path, pl.DataFrame({"v": [100, 200]}))
108
+
109
+ ws = Workspace(name="Test", ws_root_dir=folder_a)
110
+ ws.add_node(Node(data=pl.scan_parquet(parquet_path.resolve()), name="node"))
111
+ ws.save(folder_a)
112
+
113
+ # User renames the folder on disk.
114
+ folder_b = tmp_path / "Test_New"
115
+ folder_a.rename(folder_b)
116
+
117
+ # Backend flow: rebase → load → (later) save.
118
+ rebase_workspace_sources(folder_b)
119
+ ws2 = Workspace.load(folder_b)
120
+ assert len(ws2.nodes) == 1
121
+
122
+ # Verify data is accessible after rename.
123
+ node = next(iter(ws2.nodes.values()))
124
+ assert cast(pl.DataFrame, node.data.collect()).to_series().to_list() == [100, 200]
125
+
126
+ # Save (triggers GC) — the parquet must survive.
127
+ ws2.save(folder_b)
128
+ assert (folder_b / "data" / "my_data.parquet").exists()
@@ -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.4"
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