vcti-tree-exporter-csv 2.0.0__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.
- vcti_tree_exporter_csv-2.0.0/LICENSE +8 -0
- vcti_tree_exporter_csv-2.0.0/PKG-INFO +94 -0
- vcti_tree_exporter_csv-2.0.0/README.md +60 -0
- vcti_tree_exporter_csv-2.0.0/pyproject.toml +82 -0
- vcti_tree_exporter_csv-2.0.0/setup.cfg +4 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti/tree/exporter/csv/__init__.py +22 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti/tree/exporter/csv/exporter.py +232 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti/tree/exporter/csv/py.typed +0 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/PKG-INFO +94 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/SOURCES.txt +14 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/dependency_links.txt +1 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/requires.txt +14 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/top_level.txt +1 -0
- vcti_tree_exporter_csv-2.0.0/src/vcti_tree_exporter_csv.egg-info/zip-safe +1 -0
- vcti_tree_exporter_csv-2.0.0/tests/test_exporter.py +230 -0
- vcti_tree_exporter_csv-2.0.0/tests/test_version.py +15 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-tree-exporter-csv
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: CSV exporter plugin for vcti-tree-exporter: writes a vcti tree as a directory tree of CSV tables mirroring the hierarchy, with each node's attributes in a JSON sidecar.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/vcollab/vcti-python-tree-exporter-csv
|
|
8
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-tree-exporter-csv
|
|
9
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-tree-exporter-csv/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: tree,export,csv,serialization,exporter,plugin
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.15,>=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: vcti-tree-exporter>=2.0.0
|
|
23
|
+
Requires-Dist: vcti-tree>=2.0.0
|
|
24
|
+
Requires-Dist: vcti-datanode>=2.0.0
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
29
|
+
Provides-Extra: lint
|
|
30
|
+
Requires-Dist: ruff; extra == "lint"
|
|
31
|
+
Provides-Extra: typecheck
|
|
32
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# CSV Tree Exporter
|
|
36
|
+
|
|
37
|
+
CSV exporter plugin for [`vcti-tree-exporter`](https://github.com/vcollab/vcti-python-tree-exporter): writes a vcti tree as a directory tree of CSV tables.
|
|
38
|
+
|
|
39
|
+
`CsvExporter` serializes any [`vcti-tree`](https://github.com/vcollab/vcti-python-tree)
|
|
40
|
+
tree whose node payloads are
|
|
41
|
+
[`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode) `DataNode`s
|
|
42
|
+
as a directory of CSV tables mirroring the hierarchy, and ships a
|
|
43
|
+
`get_exporter_descriptor()` factory so a consumer registers it in a catalog and
|
|
44
|
+
resolves it by id (`"csv"`) or by attribute lookup.
|
|
45
|
+
|
|
46
|
+
CSV suits **light 2-D tables of settings and metadata** that load straight into
|
|
47
|
+
pandas — not heavy result arrays. Use HDF5 or NPZ for complete numeric data.
|
|
48
|
+
|
|
49
|
+
## Tree → CSV mapping
|
|
50
|
+
|
|
51
|
+
The output path is a directory:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
out/
|
|
55
|
+
_attributes.json # the root node's attributes
|
|
56
|
+
info.attributes.json # a metadata-only leaf -> sidecar only
|
|
57
|
+
materials/ # a group node -> sub-directory
|
|
58
|
+
props.csv # a structured-array leaf -> table with a header row
|
|
59
|
+
props.attributes.json # its attributes
|
|
60
|
+
vec.csv # a plain 1-D array -> one value per row
|
|
61
|
+
grid.csv # a plain 2-D array -> rows
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Tree node | On disk |
|
|
65
|
+
|---|---|
|
|
66
|
+
| node with children | sub-directory named after the node |
|
|
67
|
+
| data leaf | `<name>.csv` (structured → header + rows; plain 1-D/2-D → rows) |
|
|
68
|
+
| a node's `attributes` | `<name>.attributes.json` (leaf) or `_attributes.json` (group) |
|
|
69
|
+
| a group node's own array | `_data.csv` inside its directory |
|
|
70
|
+
|
|
71
|
+
Names are sanitized to filesystem-safe segments and de-collided case-insensitively
|
|
72
|
+
(`Mat`, `mat` → `Mat`, `mat~1`) so nothing is silently overwritten. An array with
|
|
73
|
+
more than two dimensions raises `UnrepresentableNodeError`. CSV does not preserve
|
|
74
|
+
dtype and the export is one-way.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
from vcti.tree.exporter.core import build_registry, get_exporter
|
|
81
|
+
from vcti.tree.exporter.csv import get_exporter_descriptor
|
|
82
|
+
|
|
83
|
+
registry = build_registry([get_exporter_descriptor()]) # plus any other format plugins
|
|
84
|
+
get_exporter(registry, "csv").export(tree, Path("out_dir"), overwrite=True)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`overwrite=True` replaces the existing output directory — point it at a
|
|
88
|
+
dedicated path.
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
- [vcti-tree-exporter](https://github.com/vcollab/vcti-python-tree-exporter) — the `Exporter` protocol and the `ExporterDescriptor` catalog.
|
|
93
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) / [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the tree and payload types.
|
|
94
|
+
- numpy. (CSV writing itself uses only the standard library.)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# CSV Tree Exporter
|
|
2
|
+
|
|
3
|
+
CSV exporter plugin for [`vcti-tree-exporter`](https://github.com/vcollab/vcti-python-tree-exporter): writes a vcti tree as a directory tree of CSV tables.
|
|
4
|
+
|
|
5
|
+
`CsvExporter` serializes any [`vcti-tree`](https://github.com/vcollab/vcti-python-tree)
|
|
6
|
+
tree whose node payloads are
|
|
7
|
+
[`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode) `DataNode`s
|
|
8
|
+
as a directory of CSV tables mirroring the hierarchy, and ships a
|
|
9
|
+
`get_exporter_descriptor()` factory so a consumer registers it in a catalog and
|
|
10
|
+
resolves it by id (`"csv"`) or by attribute lookup.
|
|
11
|
+
|
|
12
|
+
CSV suits **light 2-D tables of settings and metadata** that load straight into
|
|
13
|
+
pandas — not heavy result arrays. Use HDF5 or NPZ for complete numeric data.
|
|
14
|
+
|
|
15
|
+
## Tree → CSV mapping
|
|
16
|
+
|
|
17
|
+
The output path is a directory:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
out/
|
|
21
|
+
_attributes.json # the root node's attributes
|
|
22
|
+
info.attributes.json # a metadata-only leaf -> sidecar only
|
|
23
|
+
materials/ # a group node -> sub-directory
|
|
24
|
+
props.csv # a structured-array leaf -> table with a header row
|
|
25
|
+
props.attributes.json # its attributes
|
|
26
|
+
vec.csv # a plain 1-D array -> one value per row
|
|
27
|
+
grid.csv # a plain 2-D array -> rows
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Tree node | On disk |
|
|
31
|
+
|---|---|
|
|
32
|
+
| node with children | sub-directory named after the node |
|
|
33
|
+
| data leaf | `<name>.csv` (structured → header + rows; plain 1-D/2-D → rows) |
|
|
34
|
+
| a node's `attributes` | `<name>.attributes.json` (leaf) or `_attributes.json` (group) |
|
|
35
|
+
| a group node's own array | `_data.csv` inside its directory |
|
|
36
|
+
|
|
37
|
+
Names are sanitized to filesystem-safe segments and de-collided case-insensitively
|
|
38
|
+
(`Mat`, `mat` → `Mat`, `mat~1`) so nothing is silently overwritten. An array with
|
|
39
|
+
more than two dimensions raises `UnrepresentableNodeError`. CSV does not preserve
|
|
40
|
+
dtype and the export is one-way.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from vcti.tree.exporter.core import build_registry, get_exporter
|
|
47
|
+
from vcti.tree.exporter.csv import get_exporter_descriptor
|
|
48
|
+
|
|
49
|
+
registry = build_registry([get_exporter_descriptor()]) # plus any other format plugins
|
|
50
|
+
get_exporter(registry, "csv").export(tree, Path("out_dir"), overwrite=True)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`overwrite=True` replaces the existing output directory — point it at a
|
|
54
|
+
dedicated path.
|
|
55
|
+
|
|
56
|
+
## Dependencies
|
|
57
|
+
|
|
58
|
+
- [vcti-tree-exporter](https://github.com/vcollab/vcti-python-tree-exporter) — the `Exporter` protocol and the `ExporterDescriptor` catalog.
|
|
59
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) / [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the tree and payload types.
|
|
60
|
+
- numpy. (CSV writing itself uses only the standard library.)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vcti-tree-exporter-csv"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "CSV exporter plugin for vcti-tree-exporter: writes a vcti tree as a directory tree of CSV tables mirroring the hierarchy, with each node's attributes in a JSON sidecar."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Visual Collaboration Technologies Inc."}
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.12,<3.15"
|
|
14
|
+
license = "LicenseRef-Proprietary"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
keywords = ["tree", "export", "csv", "serialization", "exporter", "plugin"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"vcti-tree-exporter>=2.0.0",
|
|
29
|
+
"vcti-tree>=2.0.0",
|
|
30
|
+
"vcti-datanode>=2.0.0",
|
|
31
|
+
"numpy>=1.24",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/vcollab/vcti-python-tree-exporter-csv"
|
|
36
|
+
Repository = "https://github.com/vcollab/vcti-python-tree-exporter-csv"
|
|
37
|
+
Changelog = "https://github.com/vcollab/vcti-python-tree-exporter-csv/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
include = ["vcti.tree.exporter.csv", "vcti.tree.exporter.csv.*"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
"vcti.tree.exporter.csv" = ["py.typed"]
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
test = ["pytest", "pytest-cov"]
|
|
48
|
+
lint = ["ruff"]
|
|
49
|
+
typecheck = ["mypy"]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
zip-safe = true
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
addopts = "--cov=vcti.tree.exporter.csv --cov-report=term-missing --cov-fail-under=95"
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
python_version = "3.12"
|
|
59
|
+
strict = true
|
|
60
|
+
files = ["src"]
|
|
61
|
+
namespace_packages = true
|
|
62
|
+
explicit_package_bases = true
|
|
63
|
+
mypy_path = ["src"]
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
branch = true
|
|
67
|
+
|
|
68
|
+
[tool.coverage.report]
|
|
69
|
+
exclude_also = [
|
|
70
|
+
"raise NotImplementedError",
|
|
71
|
+
"if TYPE_CHECKING:",
|
|
72
|
+
"if __name__ == .__main__.:",
|
|
73
|
+
"@(abc\\.)?abstractmethod",
|
|
74
|
+
"\\.\\.\\.",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
target-version = "py312"
|
|
79
|
+
line-length = 99
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint]
|
|
82
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.tree.exporter.csv — CSV exporter plugin for vcti-tree-exporter.
|
|
4
|
+
|
|
5
|
+
Provides :class:`CsvExporter`, which serializes a ``vcti.tree`` tree of
|
|
6
|
+
``vcti.datanode.DataNode`` payloads as a directory tree of CSV tables mirroring
|
|
7
|
+
the hierarchy, with each node's attributes in a JSON sidecar, and
|
|
8
|
+
:func:`get_exporter_descriptor`, the catalog factory a consumer registers with
|
|
9
|
+
``vcti.tree.exporter.core.build_registry``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import version
|
|
13
|
+
|
|
14
|
+
from .exporter import CsvExporter, get_exporter_descriptor
|
|
15
|
+
|
|
16
|
+
__version__ = version("vcti-tree-exporter-csv")
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CsvExporter",
|
|
20
|
+
"__version__",
|
|
21
|
+
"get_exporter_descriptor",
|
|
22
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""CSV exporter — write a vcti tree as a directory tree of CSV tables.
|
|
4
|
+
|
|
5
|
+
Layout: the output path is a directory mirroring the tree. A node with children
|
|
6
|
+
is a sub-directory; a data leaf is a ``<name>.csv`` table. Each node's free-form
|
|
7
|
+
``attributes`` are written next to it as a JSON sidecar — ``<name>.attributes.json``
|
|
8
|
+
for a leaf, and the reserved ``_attributes.json`` / ``_data.csv`` inside a
|
|
9
|
+
directory for a group node's own attributes/array.
|
|
10
|
+
|
|
11
|
+
CSV suits 2-D tables of settings and metadata that load straight into pandas.
|
|
12
|
+
It does not preserve dtype, and an array with more than two dimensions (or a
|
|
13
|
+
structured array that is not 1-D) raises ``UnrepresentableNodeError``. The
|
|
14
|
+
export is one-way (no matching importer).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import csv
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
from vcti.datanode import DataNode
|
|
29
|
+
from vcti.tree.exporter.core import (
|
|
30
|
+
EXTENSION_ATTR,
|
|
31
|
+
FORMAT_ATTR,
|
|
32
|
+
ExporterDescriptor,
|
|
33
|
+
UnrepresentableNodeError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from vcti.tree.core import ReadOnlyTree
|
|
38
|
+
|
|
39
|
+
#: Fixed, well-known id for the CSV exporter descriptor.
|
|
40
|
+
EXPORTER_ID = "csv"
|
|
41
|
+
|
|
42
|
+
#: Reserved file names for a group/root node's own array and attributes.
|
|
43
|
+
DATA_FILE = "_data.csv"
|
|
44
|
+
ATTRS_FILE = "_attributes.json"
|
|
45
|
+
|
|
46
|
+
_UNSAFE = re.compile(r"[^A-Za-z0-9._-]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _json_default(value: Any) -> Any:
|
|
50
|
+
"""Coerce a value JSON cannot serialize natively into a JSON-safe form."""
|
|
51
|
+
if isinstance(value, bytes):
|
|
52
|
+
return value.decode("utf-8", errors="replace").rstrip("\x00")
|
|
53
|
+
if isinstance(value, np.generic):
|
|
54
|
+
return value.item()
|
|
55
|
+
if isinstance(value, np.ndarray):
|
|
56
|
+
return value.tolist()
|
|
57
|
+
return str(value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _csv_value(value: Any) -> Any:
|
|
61
|
+
"""Coerce a single cell value into a CSV-friendly form.
|
|
62
|
+
|
|
63
|
+
``csv.writer`` already stringifies numpy scalars sensibly; only ``bytes``
|
|
64
|
+
need decoding so they are not written as a ``b'...'`` repr.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(value, bytes):
|
|
67
|
+
return value.decode("utf-8", errors="replace").rstrip("\x00")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _safe_name(name: str) -> str:
|
|
72
|
+
"""Sanitize a node name into a filesystem-safe path segment.
|
|
73
|
+
|
|
74
|
+
Replaces characters outside ``[A-Za-z0-9._-]`` and strips leading/trailing
|
|
75
|
+
dots and spaces, so a name such as ``".."`` or ``"../x"`` cannot escape the
|
|
76
|
+
output directory (path traversal) and Windows trailing-dot/space trimming
|
|
77
|
+
cannot silently collide two names.
|
|
78
|
+
"""
|
|
79
|
+
safe = _UNSAFE.sub("_", name).strip(". ")
|
|
80
|
+
return safe or "node"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _write_attrs(attrs: dict[str, Any], path: Path) -> None:
|
|
84
|
+
with path.open("w", encoding="utf-8") as f:
|
|
85
|
+
json.dump(attrs, f, indent=2, ensure_ascii=False, default=_json_default)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _write_csv(array: NDArray[Any], path: Path) -> None:
|
|
89
|
+
"""Write a 1-D/2-D (or structured 1-D) array as a CSV table."""
|
|
90
|
+
names = array.dtype.names
|
|
91
|
+
if array.ndim > 2 or (names is not None and array.ndim > 1):
|
|
92
|
+
raise UnrepresentableNodeError(
|
|
93
|
+
f"array with shape {array.shape} and dtype {array.dtype} "
|
|
94
|
+
"cannot be written as a 2-D CSV table"
|
|
95
|
+
)
|
|
96
|
+
with path.open("w", newline="", encoding="utf-8") as f:
|
|
97
|
+
writer = csv.writer(f)
|
|
98
|
+
if names is not None:
|
|
99
|
+
writer.writerow(names)
|
|
100
|
+
for record in np.atleast_1d(array):
|
|
101
|
+
writer.writerow([_csv_value(v) for v in record])
|
|
102
|
+
elif array.ndim == 2:
|
|
103
|
+
for row in array:
|
|
104
|
+
writer.writerow([_csv_value(v) for v in row])
|
|
105
|
+
else: # 0-D or 1-D plain
|
|
106
|
+
for value in np.atleast_1d(array):
|
|
107
|
+
writer.writerow([_csv_value(value)])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CsvExporter:
|
|
111
|
+
"""Serialize a vcti tree as a directory tree of CSV tables.
|
|
112
|
+
|
|
113
|
+
Satisfies the ``vcti.tree.exporter.core.Exporter`` protocol structurally. Format
|
|
114
|
+
name and (empty) default extension live on its descriptor
|
|
115
|
+
(:func:`get_exporter_descriptor`), not on this class.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def export(
|
|
119
|
+
self,
|
|
120
|
+
tree: ReadOnlyTree[DataNode, Any],
|
|
121
|
+
path: Path,
|
|
122
|
+
*,
|
|
123
|
+
overwrite: bool = False,
|
|
124
|
+
) -> None:
|
|
125
|
+
path = Path(path)
|
|
126
|
+
if path.exists():
|
|
127
|
+
if not overwrite:
|
|
128
|
+
raise FileExistsError(f"Output path already exists: {path}")
|
|
129
|
+
self._remove(path)
|
|
130
|
+
|
|
131
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
path.mkdir()
|
|
133
|
+
self._render_dir(tree, tree.root_handle, path)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _remove(path: Path) -> None:
|
|
137
|
+
if path.is_dir():
|
|
138
|
+
shutil.rmtree(path)
|
|
139
|
+
else:
|
|
140
|
+
path.unlink()
|
|
141
|
+
|
|
142
|
+
def _render_dir(
|
|
143
|
+
self,
|
|
144
|
+
tree: ReadOnlyTree[DataNode, Any],
|
|
145
|
+
handle: Any,
|
|
146
|
+
directory: Path,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Write a group/root node's own content into ``directory``, then children."""
|
|
149
|
+
payload = tree.payload(handle)
|
|
150
|
+
if isinstance(payload, DataNode):
|
|
151
|
+
self._write_own(payload, directory)
|
|
152
|
+
|
|
153
|
+
used: set[str] = {"_data", "_attributes"} # reserved stems in this directory
|
|
154
|
+
for index, child in enumerate(tree.children(handle)):
|
|
155
|
+
self._render_child(tree, child, directory, index, used)
|
|
156
|
+
|
|
157
|
+
def _write_own(self, payload: DataNode, directory: Path) -> None:
|
|
158
|
+
"""Write a group/root node's reserved ``_attributes.json`` / ``_data.csv``."""
|
|
159
|
+
attrs = dict(payload.attributes)
|
|
160
|
+
if attrs:
|
|
161
|
+
_write_attrs(attrs, directory / ATTRS_FILE)
|
|
162
|
+
array = self._take_array(payload)
|
|
163
|
+
if array is not None:
|
|
164
|
+
_write_csv(array, directory / DATA_FILE)
|
|
165
|
+
|
|
166
|
+
def _render_child(
|
|
167
|
+
self,
|
|
168
|
+
tree: ReadOnlyTree[DataNode, Any],
|
|
169
|
+
handle: Any,
|
|
170
|
+
parent_dir: Path,
|
|
171
|
+
index: int,
|
|
172
|
+
used: set[str],
|
|
173
|
+
) -> None:
|
|
174
|
+
payload = tree.payload(handle)
|
|
175
|
+
if not isinstance(payload, DataNode):
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
stem = _unique_stem(_safe_name(payload.name or f"node_{index}"), used)
|
|
179
|
+
children = list(tree.children(handle))
|
|
180
|
+
|
|
181
|
+
if children:
|
|
182
|
+
subdir = parent_dir / stem
|
|
183
|
+
subdir.mkdir()
|
|
184
|
+
self._render_dir(tree, handle, subdir)
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
attrs = dict(payload.attributes)
|
|
188
|
+
array = self._take_array(payload)
|
|
189
|
+
if array is not None:
|
|
190
|
+
_write_csv(array, parent_dir / f"{stem}.csv")
|
|
191
|
+
if attrs:
|
|
192
|
+
_write_attrs(attrs, parent_dir / f"{stem}.attributes.json")
|
|
193
|
+
elif attrs:
|
|
194
|
+
_write_attrs(attrs, parent_dir / f"{stem}.attributes.json")
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _take_array(payload: DataNode) -> NDArray[Any] | None:
|
|
198
|
+
"""Materialise the node's array, releasing a lazy source we loaded."""
|
|
199
|
+
was_loaded = payload.is_loaded
|
|
200
|
+
array = payload.load()
|
|
201
|
+
if array is not None and not was_loaded:
|
|
202
|
+
payload.unload()
|
|
203
|
+
return array
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unique_stem(name: str, used: set[str]) -> str:
|
|
207
|
+
"""Return ``name`` (or a ``name~N`` variant) unused in this directory.
|
|
208
|
+
|
|
209
|
+
Comparison is case-insensitive so two siblings differing only in case do
|
|
210
|
+
not collide on a case-insensitive filesystem.
|
|
211
|
+
"""
|
|
212
|
+
candidate = name
|
|
213
|
+
n = 1
|
|
214
|
+
while candidate.lower() in used:
|
|
215
|
+
candidate = f"{name}~{n}"
|
|
216
|
+
n += 1
|
|
217
|
+
used.add(candidate.lower())
|
|
218
|
+
return candidate
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_exporter_descriptor() -> ExporterDescriptor:
|
|
222
|
+
"""Create the :class:`ExporterDescriptor` for the CSV exporter."""
|
|
223
|
+
return ExporterDescriptor(
|
|
224
|
+
id=EXPORTER_ID,
|
|
225
|
+
name="CSV Exporter",
|
|
226
|
+
exporter=CsvExporter(),
|
|
227
|
+
description="Writes a vcti tree as a directory of CSV tables with JSON sidecars.",
|
|
228
|
+
attributes={
|
|
229
|
+
FORMAT_ATTR: "csv",
|
|
230
|
+
EXTENSION_ATTR: "", # writes a directory, not a single file
|
|
231
|
+
},
|
|
232
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-tree-exporter-csv
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: CSV exporter plugin for vcti-tree-exporter: writes a vcti tree as a directory tree of CSV tables mirroring the hierarchy, with each node's attributes in a JSON sidecar.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/vcollab/vcti-python-tree-exporter-csv
|
|
8
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-tree-exporter-csv
|
|
9
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-tree-exporter-csv/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: tree,export,csv,serialization,exporter,plugin
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.15,>=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: vcti-tree-exporter>=2.0.0
|
|
23
|
+
Requires-Dist: vcti-tree>=2.0.0
|
|
24
|
+
Requires-Dist: vcti-datanode>=2.0.0
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
29
|
+
Provides-Extra: lint
|
|
30
|
+
Requires-Dist: ruff; extra == "lint"
|
|
31
|
+
Provides-Extra: typecheck
|
|
32
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# CSV Tree Exporter
|
|
36
|
+
|
|
37
|
+
CSV exporter plugin for [`vcti-tree-exporter`](https://github.com/vcollab/vcti-python-tree-exporter): writes a vcti tree as a directory tree of CSV tables.
|
|
38
|
+
|
|
39
|
+
`CsvExporter` serializes any [`vcti-tree`](https://github.com/vcollab/vcti-python-tree)
|
|
40
|
+
tree whose node payloads are
|
|
41
|
+
[`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode) `DataNode`s
|
|
42
|
+
as a directory of CSV tables mirroring the hierarchy, and ships a
|
|
43
|
+
`get_exporter_descriptor()` factory so a consumer registers it in a catalog and
|
|
44
|
+
resolves it by id (`"csv"`) or by attribute lookup.
|
|
45
|
+
|
|
46
|
+
CSV suits **light 2-D tables of settings and metadata** that load straight into
|
|
47
|
+
pandas — not heavy result arrays. Use HDF5 or NPZ for complete numeric data.
|
|
48
|
+
|
|
49
|
+
## Tree → CSV mapping
|
|
50
|
+
|
|
51
|
+
The output path is a directory:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
out/
|
|
55
|
+
_attributes.json # the root node's attributes
|
|
56
|
+
info.attributes.json # a metadata-only leaf -> sidecar only
|
|
57
|
+
materials/ # a group node -> sub-directory
|
|
58
|
+
props.csv # a structured-array leaf -> table with a header row
|
|
59
|
+
props.attributes.json # its attributes
|
|
60
|
+
vec.csv # a plain 1-D array -> one value per row
|
|
61
|
+
grid.csv # a plain 2-D array -> rows
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Tree node | On disk |
|
|
65
|
+
|---|---|
|
|
66
|
+
| node with children | sub-directory named after the node |
|
|
67
|
+
| data leaf | `<name>.csv` (structured → header + rows; plain 1-D/2-D → rows) |
|
|
68
|
+
| a node's `attributes` | `<name>.attributes.json` (leaf) or `_attributes.json` (group) |
|
|
69
|
+
| a group node's own array | `_data.csv` inside its directory |
|
|
70
|
+
|
|
71
|
+
Names are sanitized to filesystem-safe segments and de-collided case-insensitively
|
|
72
|
+
(`Mat`, `mat` → `Mat`, `mat~1`) so nothing is silently overwritten. An array with
|
|
73
|
+
more than two dimensions raises `UnrepresentableNodeError`. CSV does not preserve
|
|
74
|
+
dtype and the export is one-way.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
from vcti.tree.exporter.core import build_registry, get_exporter
|
|
81
|
+
from vcti.tree.exporter.csv import get_exporter_descriptor
|
|
82
|
+
|
|
83
|
+
registry = build_registry([get_exporter_descriptor()]) # plus any other format plugins
|
|
84
|
+
get_exporter(registry, "csv").export(tree, Path("out_dir"), overwrite=True)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`overwrite=True` replaces the existing output directory — point it at a
|
|
88
|
+
dedicated path.
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
- [vcti-tree-exporter](https://github.com/vcollab/vcti-python-tree-exporter) — the `Exporter` protocol and the `ExporterDescriptor` catalog.
|
|
93
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) / [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the tree and payload types.
|
|
94
|
+
- numpy. (CSV writing itself uses only the standard library.)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/vcti/tree/exporter/csv/__init__.py
|
|
5
|
+
src/vcti/tree/exporter/csv/exporter.py
|
|
6
|
+
src/vcti/tree/exporter/csv/py.typed
|
|
7
|
+
src/vcti_tree_exporter_csv.egg-info/PKG-INFO
|
|
8
|
+
src/vcti_tree_exporter_csv.egg-info/SOURCES.txt
|
|
9
|
+
src/vcti_tree_exporter_csv.egg-info/dependency_links.txt
|
|
10
|
+
src/vcti_tree_exporter_csv.egg-info/requires.txt
|
|
11
|
+
src/vcti_tree_exporter_csv.egg-info/top_level.txt
|
|
12
|
+
src/vcti_tree_exporter_csv.egg-info/zip-safe
|
|
13
|
+
tests/test_exporter.py
|
|
14
|
+
tests/test_version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Tests for the CSV exporter."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
from vcti.datanode import DataNode, EagerDataSource, LazyDataSource
|
|
13
|
+
from vcti.lookup import Rule
|
|
14
|
+
from vcti.tree.core import DictTree
|
|
15
|
+
from vcti.tree.exporter.core import (
|
|
16
|
+
EXTENSION_ATTR,
|
|
17
|
+
FORMAT_ATTR,
|
|
18
|
+
Exporter,
|
|
19
|
+
UnrepresentableNodeError,
|
|
20
|
+
build_registry,
|
|
21
|
+
get_exporter,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from vcti.tree.exporter.csv import CsvExporter, get_exporter_descriptor
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _data(array: np.ndarray, *, name: str) -> DataNode:
|
|
28
|
+
return DataNode(name=name, data_source=EagerDataSource(array))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_csv(path):
|
|
32
|
+
with path.open(newline="", encoding="utf-8") as f:
|
|
33
|
+
return list(csv.reader(f))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_json(path):
|
|
37
|
+
with path.open(encoding="utf-8") as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sample_tree() -> DictTree:
|
|
42
|
+
tree: DictTree = DictTree(DataNode(name="model", source_attributes={"solver": "abaqus"}))
|
|
43
|
+
root = tree.root_handle
|
|
44
|
+
|
|
45
|
+
# metadata-only leaf -> sidecar only
|
|
46
|
+
tree.create_child(root, DataNode(name="info", source_attributes={"count": np.int64(7)}))
|
|
47
|
+
|
|
48
|
+
# group with a structured-table leaf carrying attributes
|
|
49
|
+
materials = tree.create_child(root, DataNode(name="materials"))
|
|
50
|
+
table = np.array([("Steel", 210.0), ("Al", 70.0)], dtype=[("name", "U8"), ("E", "f8")])
|
|
51
|
+
tree.create_child(
|
|
52
|
+
materials,
|
|
53
|
+
DataNode(
|
|
54
|
+
name="props",
|
|
55
|
+
data_source=EagerDataSource(table),
|
|
56
|
+
source_attributes={"unit": "GPa"},
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# plain 1-D and 2-D leaves at the root
|
|
61
|
+
tree.create_child(root, _data(np.array([1.0, 2.0, 3.0]), name="vec"))
|
|
62
|
+
tree.create_child(root, _data(np.arange(6).reshape(2, 3), name="grid"))
|
|
63
|
+
|
|
64
|
+
# empty leaf writes nothing; non-DataNode payload is skipped
|
|
65
|
+
tree.create_child(root, DataNode(name="empty"))
|
|
66
|
+
tree.create_child(root, "not-a-datanode")
|
|
67
|
+
return tree
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _export(tmp_path, tree, name="out"):
|
|
71
|
+
out = tmp_path / name
|
|
72
|
+
CsvExporter().export(tree, out)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_protocol_conformance():
|
|
77
|
+
assert isinstance(CsvExporter(), Exporter)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_descriptor_factory_records_metadata():
|
|
81
|
+
desc = get_exporter_descriptor()
|
|
82
|
+
assert desc.id == "csv"
|
|
83
|
+
assert isinstance(desc.exporter, CsvExporter)
|
|
84
|
+
assert desc.attributes[FORMAT_ATTR] == "csv"
|
|
85
|
+
assert desc.attributes[EXTENSION_ATTR] == "" # directory output
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_root_attributes_and_directory_layout(tmp_path):
|
|
89
|
+
out = _export(tmp_path, _sample_tree())
|
|
90
|
+
# root attributes -> reserved sidecar in the top directory
|
|
91
|
+
assert _read_json(out / "_attributes.json") == {"solver": "abaqus"}
|
|
92
|
+
# group node -> sub-directory
|
|
93
|
+
assert (out / "materials").is_dir()
|
|
94
|
+
# empty leaf produced nothing
|
|
95
|
+
assert not (out / "empty.csv").exists()
|
|
96
|
+
assert not (out / "empty.attributes.json").exists()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_metadata_only_leaf_writes_sidecar_only(tmp_path):
|
|
100
|
+
out = _export(tmp_path, _sample_tree())
|
|
101
|
+
assert not (out / "info.csv").exists()
|
|
102
|
+
assert _read_json(out / "info.attributes.json") == {"count": 7}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_structured_table_has_header_and_sidecar(tmp_path):
|
|
106
|
+
out = _export(tmp_path, _sample_tree())
|
|
107
|
+
rows = _read_csv(out / "materials" / "props.csv")
|
|
108
|
+
assert rows[0] == ["name", "E"] # field-name header
|
|
109
|
+
assert rows[1] == ["Steel", "210.0"]
|
|
110
|
+
assert _read_json(out / "materials" / "props.attributes.json") == {"unit": "GPa"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_plain_1d_and_2d_arrays(tmp_path):
|
|
114
|
+
out = _export(tmp_path, _sample_tree())
|
|
115
|
+
assert _read_csv(out / "vec.csv") == [["1.0"], ["2.0"], ["3.0"]]
|
|
116
|
+
assert _read_csv(out / "grid.csv") == [["0", "1", "2"], ["3", "4", "5"]]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_node_with_both_data_and_children(tmp_path):
|
|
120
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
121
|
+
parent = tree.create_child(
|
|
122
|
+
tree.root_handle,
|
|
123
|
+
DataNode(name="parent", data_source=EagerDataSource(np.array([9]))),
|
|
124
|
+
)
|
|
125
|
+
tree.create_child(parent, DataNode(name="kid", source_attributes={"k": 1}))
|
|
126
|
+
out = _export(tmp_path, tree)
|
|
127
|
+
# the group's own array lands in the reserved _data.csv inside its directory
|
|
128
|
+
assert _read_csv(out / "parent" / "_data.csv") == [["9"]]
|
|
129
|
+
assert _read_json(out / "parent" / "kid.attributes.json") == {"k": 1}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_attribute_values_are_json_encoded(tmp_path):
|
|
133
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
134
|
+
tree.create_child(
|
|
135
|
+
tree.root_handle,
|
|
136
|
+
DataNode(
|
|
137
|
+
name="n",
|
|
138
|
+
source_attributes={
|
|
139
|
+
"label": b"ABAQUS", # bytes -> decoded
|
|
140
|
+
"vec": np.array([1.0, 2.0]), # ndarray -> list
|
|
141
|
+
"tags": {"a", "b"}, # set -> str() fallback
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
out = _export(tmp_path, tree)
|
|
146
|
+
attrs = _read_json(out / "n.attributes.json")
|
|
147
|
+
assert attrs["label"] == "ABAQUS"
|
|
148
|
+
assert attrs["vec"] == [1.0, 2.0]
|
|
149
|
+
assert isinstance(attrs["tags"], str)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_bytes_cells_are_decoded(tmp_path):
|
|
153
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
154
|
+
tree.create_child(tree.root_handle, _data(np.array([b"mm", b"t"]), name="units"))
|
|
155
|
+
out = _export(tmp_path, tree)
|
|
156
|
+
assert _read_csv(out / "units.csv") == [["mm"], ["t"]]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_traversal_names_are_neutralised(tmp_path):
|
|
160
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
161
|
+
tree.create_child(tree.root_handle, _data(np.array([1]), name=".."))
|
|
162
|
+
tree.create_child(tree.root_handle, _data(np.array([2]), name="../escape"))
|
|
163
|
+
out = _export(tmp_path, tree)
|
|
164
|
+
|
|
165
|
+
# nothing escaped the output directory
|
|
166
|
+
assert list(out.parent.iterdir()) == [out]
|
|
167
|
+
assert sorted(p.name for p in out.glob("*.csv")) == ["_escape.csv", "node.csv"]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_non_datanode_root_writes_empty_directory(tmp_path):
|
|
171
|
+
tree: DictTree = DictTree("not-a-datanode")
|
|
172
|
+
out = _export(tmp_path, tree)
|
|
173
|
+
assert out.is_dir()
|
|
174
|
+
assert list(out.iterdir()) == []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_more_than_2d_raises(tmp_path):
|
|
178
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
179
|
+
tree.create_child(tree.root_handle, _data(np.zeros((2, 2, 2)), name="cube"))
|
|
180
|
+
with pytest.raises(UnrepresentableNodeError, match="2-D CSV table"):
|
|
181
|
+
CsvExporter().export(tree, tmp_path / "out")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_name_sanitised_and_case_collision_deduped(tmp_path):
|
|
185
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
186
|
+
tree.create_child(tree.root_handle, _data(np.array([1]), name="a/b")) # unsafe '/'
|
|
187
|
+
tree.create_child(tree.root_handle, _data(np.array([2]), name="Mat"))
|
|
188
|
+
tree.create_child(tree.root_handle, _data(np.array([3]), name="mat")) # case-collides
|
|
189
|
+
out = _export(tmp_path, tree)
|
|
190
|
+
names = sorted(p.name for p in out.glob("*.csv"))
|
|
191
|
+
assert names == ["Mat.csv", "a_b.csv", "mat~1.csv"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_lazy_node_materialised_then_released(tmp_path):
|
|
195
|
+
tree: DictTree = DictTree(DataNode(name="root"))
|
|
196
|
+
node = DataNode(name="lazy", data_source=LazyDataSource(lambda: np.array([5, 6]), shape=(2,)))
|
|
197
|
+
tree.create_child(tree.root_handle, node)
|
|
198
|
+
assert not node.is_loaded
|
|
199
|
+
out = _export(tmp_path, tree)
|
|
200
|
+
assert _read_csv(out / "lazy.csv") == [["5"], ["6"]]
|
|
201
|
+
assert not node.is_loaded # restored
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_overwrite_behavior(tmp_path):
|
|
205
|
+
tree = _sample_tree()
|
|
206
|
+
out = tmp_path / "out"
|
|
207
|
+
CsvExporter().export(tree, out)
|
|
208
|
+
with pytest.raises(FileExistsError):
|
|
209
|
+
CsvExporter().export(tree, out)
|
|
210
|
+
CsvExporter().export(tree, out, overwrite=True) # replaces the directory
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_overwrite_replaces_a_file_at_the_path(tmp_path):
|
|
214
|
+
out = tmp_path / "out"
|
|
215
|
+
out.write_text("i am a file")
|
|
216
|
+
CsvExporter().export(_sample_tree(), out, overwrite=True)
|
|
217
|
+
assert out.is_dir()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_creates_parent_directories(tmp_path):
|
|
221
|
+
out = tmp_path / "nested" / "deep" / "out"
|
|
222
|
+
CsvExporter().export(_sample_tree(), out)
|
|
223
|
+
assert (out / "_attributes.json").exists()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_registry_integration():
|
|
227
|
+
registry = build_registry([get_exporter_descriptor()])
|
|
228
|
+
assert isinstance(get_exporter(registry, "csv"), CsvExporter)
|
|
229
|
+
(desc,) = registry.lookup.filter([Rule(FORMAT_ATTR, "==", "csv")])
|
|
230
|
+
assert desc.id == "csv"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Version tests for vcti-tree-exporter-csv."""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import vcti.tree.exporter.csv
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestVersion:
|
|
11
|
+
def test_version_exists(self):
|
|
12
|
+
assert hasattr(vcti.tree.exporter.csv, "__version__")
|
|
13
|
+
|
|
14
|
+
def test_version_is_valid_semver(self):
|
|
15
|
+
assert re.match(r"^\d+\.\d+\.\d+", vcti.tree.exporter.csv.__version__)
|