vcti-tree-exporter 1.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-1.0.0/LICENSE +8 -0
- vcti_tree_exporter-1.0.0/PKG-INFO +138 -0
- vcti_tree_exporter-1.0.0/README.md +105 -0
- vcti_tree_exporter-1.0.0/pyproject.toml +81 -0
- vcti_tree_exporter-1.0.0/setup.cfg +4 -0
- vcti_tree_exporter-1.0.0/src/vcti/treeexporter/__init__.py +46 -0
- vcti_tree_exporter-1.0.0/src/vcti/treeexporter/exceptions.py +28 -0
- vcti_tree_exporter-1.0.0/src/vcti/treeexporter/protocol.py +62 -0
- vcti_tree_exporter-1.0.0/src/vcti/treeexporter/py.typed +0 -0
- vcti_tree_exporter-1.0.0/src/vcti/treeexporter/registry.py +99 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/PKG-INFO +138 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/SOURCES.txt +16 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/dependency_links.txt +1 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/requires.txt +13 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/top_level.txt +1 -0
- vcti_tree_exporter-1.0.0/src/vcti_tree_exporter.egg-info/zip-safe +1 -0
- vcti_tree_exporter-1.0.0/tests/test_registry.py +128 -0
- vcti_tree_exporter-1.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,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-tree-exporter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Format-agnostic exporters for vcti trees: an Exporter protocol and registry, with per-format writers as plugins.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/vcollab/vcti-python-tree-exporter
|
|
8
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-tree-exporter
|
|
9
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-tree-exporter/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: tree,export,serialization,exporter,protocol,registry,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>=1.1.0
|
|
23
|
+
Requires-Dist: vcti-datanode>=2.0.0
|
|
24
|
+
Requires-Dist: vcti-plugin-catalog>=1.0.1
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
28
|
+
Provides-Extra: lint
|
|
29
|
+
Requires-Dist: ruff; extra == "lint"
|
|
30
|
+
Provides-Extra: typecheck
|
|
31
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Tree Exporter
|
|
35
|
+
|
|
36
|
+
Format-agnostic exporters for vcti trees: an `Exporter` protocol and a plugin catalog, with per-format writers as plugins.
|
|
37
|
+
|
|
38
|
+
`vcti-tree-exporter` defines *what an exporter is* and *how to catalogue and
|
|
39
|
+
look one up*. It implements no file format itself — concrete writers (HDF5,
|
|
40
|
+
JSON, CSV, NumPy, …) live in separate plugin packages and are registered by the
|
|
41
|
+
consumer. The thing being exported is any
|
|
42
|
+
[`vcti-tree`](https://github.com/vcollab/vcti-python-tree) tree whose node
|
|
43
|
+
payloads are [`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode)
|
|
44
|
+
`DataNode`s (data + attributes), so one tree can be written to any registered
|
|
45
|
+
format without the producer knowing about serialization.
|
|
46
|
+
|
|
47
|
+
Cataloguing and lookup are built on
|
|
48
|
+
[`vcti-plugin-catalog`](https://github.com/vcollab/vcti-python-plugin-catalog)
|
|
49
|
+
(`Descriptor` + `Registry`).
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install vcti-tree-exporter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The base package is intentionally small. Install a format plugin for each
|
|
58
|
+
format you need:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install vcti-tree-exporter-hdf5 # single .h5 file
|
|
62
|
+
pip install vcti-tree-exporter-json # single .json document
|
|
63
|
+
pip install vcti-tree-exporter-npz # single .npz archive
|
|
64
|
+
pip install vcti-tree-exporter-csv # directory tree of CSV tables
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Core concepts
|
|
68
|
+
|
|
69
|
+
| Piece | Role |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `Exporter` | Structural protocol — a single `export(tree, path, *, overwrite=False)` method. Pure behavior; format name and extension are **not** on the exporter. |
|
|
72
|
+
| `ExporterDescriptor` | A `vcti-plugin-catalog` `Descriptor[Exporter]`: the exporter is its `instance`; `format`, `extension`, and any other metadata are filterable `attributes`. |
|
|
73
|
+
| `get_exporter_descriptor()` | Factory each plugin package provides, returning its `ExporterDescriptor`. |
|
|
74
|
+
| `build_registry(descriptors)` | Build a plugin-catalog `Registry` from those descriptors. |
|
|
75
|
+
| `get_exporter(registry, id)` | Resolve an exporter instance by descriptor id. |
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
Register the descriptors your application uses, then resolve one and write a
|
|
80
|
+
tree:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
from vcti.treeexporter import build_registry, get_exporter
|
|
85
|
+
from vcti.treeexporter_hdf5 import get_exporter_descriptor as hdf5_descriptor
|
|
86
|
+
from vcti.treeexporter_json import get_exporter_descriptor as json_descriptor
|
|
87
|
+
|
|
88
|
+
registry = build_registry([hdf5_descriptor(), json_descriptor()])
|
|
89
|
+
get_exporter(registry, "hdf5").export(tree, Path("model.h5"), overwrite=True)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`registry` is an ordinary `vcti.plugincatalog.Registry`, so the general way to
|
|
93
|
+
select an exporter is to filter by attributes, then use the matching id — the
|
|
94
|
+
same pattern as the rest of the ecosystem:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from vcti.lookup import Rule
|
|
98
|
+
from vcti.treeexporter import EXTENSION_ATTR
|
|
99
|
+
|
|
100
|
+
(desc,) = registry.lookup.filter([Rule(EXTENSION_ATTR, "==", ".h5")])
|
|
101
|
+
desc.exporter.export(tree, Path("model.h5"))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Exporter ids are a small, fixed, well-known set, so using `get_exporter(registry,
|
|
105
|
+
"hdf5")` directly is also fine.
|
|
106
|
+
|
|
107
|
+
## Writing a format plugin
|
|
108
|
+
|
|
109
|
+
A plugin's exporter only has to satisfy the protocol — one `export` method, no
|
|
110
|
+
base class — and ship a `get_exporter_descriptor()` factory carrying its
|
|
111
|
+
metadata:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
from vcti.treeexporter import EXTENSION_ATTR, FORMAT_ATTR, ExporterDescriptor
|
|
116
|
+
|
|
117
|
+
class JsonExporter:
|
|
118
|
+
def export(self, tree, path: Path, *, overwrite: bool = False) -> None:
|
|
119
|
+
... # walk the tree (node.name, node.attributes, node.load()), write JSON
|
|
120
|
+
|
|
121
|
+
def get_exporter_descriptor() -> ExporterDescriptor:
|
|
122
|
+
return ExporterDescriptor(
|
|
123
|
+
id="json", name="JSON Exporter", exporter=JsonExporter(),
|
|
124
|
+
attributes={FORMAT_ATTR: "json", EXTENSION_ATTR: ".json"},
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Ship it in its own package (with its own format dependencies, e.g. `h5py` for
|
|
129
|
+
HDF5). The consuming application decides which descriptors to register — there
|
|
130
|
+
is no implicit global discovery.
|
|
131
|
+
|
|
132
|
+
## Dependencies
|
|
133
|
+
|
|
134
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) — the tree protocols the exporter reads.
|
|
135
|
+
- [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the node payload type.
|
|
136
|
+
- [vcti-plugin-catalog](https://github.com/vcollab/vcti-python-plugin-catalog) — the `Descriptor` / `Registry` catalog.
|
|
137
|
+
|
|
138
|
+
(No heavy/runtime format dependencies live here — those belong to the per-format plugin packages.)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Tree Exporter
|
|
2
|
+
|
|
3
|
+
Format-agnostic exporters for vcti trees: an `Exporter` protocol and a plugin catalog, with per-format writers as plugins.
|
|
4
|
+
|
|
5
|
+
`vcti-tree-exporter` defines *what an exporter is* and *how to catalogue and
|
|
6
|
+
look one up*. It implements no file format itself — concrete writers (HDF5,
|
|
7
|
+
JSON, CSV, NumPy, …) live in separate plugin packages and are registered by the
|
|
8
|
+
consumer. The thing being exported is any
|
|
9
|
+
[`vcti-tree`](https://github.com/vcollab/vcti-python-tree) tree whose node
|
|
10
|
+
payloads are [`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode)
|
|
11
|
+
`DataNode`s (data + attributes), so one tree can be written to any registered
|
|
12
|
+
format without the producer knowing about serialization.
|
|
13
|
+
|
|
14
|
+
Cataloguing and lookup are built on
|
|
15
|
+
[`vcti-plugin-catalog`](https://github.com/vcollab/vcti-python-plugin-catalog)
|
|
16
|
+
(`Descriptor` + `Registry`).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install vcti-tree-exporter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The base package is intentionally small. Install a format plugin for each
|
|
25
|
+
format you need:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install vcti-tree-exporter-hdf5 # single .h5 file
|
|
29
|
+
pip install vcti-tree-exporter-json # single .json document
|
|
30
|
+
pip install vcti-tree-exporter-npz # single .npz archive
|
|
31
|
+
pip install vcti-tree-exporter-csv # directory tree of CSV tables
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core concepts
|
|
35
|
+
|
|
36
|
+
| Piece | Role |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `Exporter` | Structural protocol — a single `export(tree, path, *, overwrite=False)` method. Pure behavior; format name and extension are **not** on the exporter. |
|
|
39
|
+
| `ExporterDescriptor` | A `vcti-plugin-catalog` `Descriptor[Exporter]`: the exporter is its `instance`; `format`, `extension`, and any other metadata are filterable `attributes`. |
|
|
40
|
+
| `get_exporter_descriptor()` | Factory each plugin package provides, returning its `ExporterDescriptor`. |
|
|
41
|
+
| `build_registry(descriptors)` | Build a plugin-catalog `Registry` from those descriptors. |
|
|
42
|
+
| `get_exporter(registry, id)` | Resolve an exporter instance by descriptor id. |
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
Register the descriptors your application uses, then resolve one and write a
|
|
47
|
+
tree:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from vcti.treeexporter import build_registry, get_exporter
|
|
52
|
+
from vcti.treeexporter_hdf5 import get_exporter_descriptor as hdf5_descriptor
|
|
53
|
+
from vcti.treeexporter_json import get_exporter_descriptor as json_descriptor
|
|
54
|
+
|
|
55
|
+
registry = build_registry([hdf5_descriptor(), json_descriptor()])
|
|
56
|
+
get_exporter(registry, "hdf5").export(tree, Path("model.h5"), overwrite=True)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`registry` is an ordinary `vcti.plugincatalog.Registry`, so the general way to
|
|
60
|
+
select an exporter is to filter by attributes, then use the matching id — the
|
|
61
|
+
same pattern as the rest of the ecosystem:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from vcti.lookup import Rule
|
|
65
|
+
from vcti.treeexporter import EXTENSION_ATTR
|
|
66
|
+
|
|
67
|
+
(desc,) = registry.lookup.filter([Rule(EXTENSION_ATTR, "==", ".h5")])
|
|
68
|
+
desc.exporter.export(tree, Path("model.h5"))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Exporter ids are a small, fixed, well-known set, so using `get_exporter(registry,
|
|
72
|
+
"hdf5")` directly is also fine.
|
|
73
|
+
|
|
74
|
+
## Writing a format plugin
|
|
75
|
+
|
|
76
|
+
A plugin's exporter only has to satisfy the protocol — one `export` method, no
|
|
77
|
+
base class — and ship a `get_exporter_descriptor()` factory carrying its
|
|
78
|
+
metadata:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
from vcti.treeexporter import EXTENSION_ATTR, FORMAT_ATTR, ExporterDescriptor
|
|
83
|
+
|
|
84
|
+
class JsonExporter:
|
|
85
|
+
def export(self, tree, path: Path, *, overwrite: bool = False) -> None:
|
|
86
|
+
... # walk the tree (node.name, node.attributes, node.load()), write JSON
|
|
87
|
+
|
|
88
|
+
def get_exporter_descriptor() -> ExporterDescriptor:
|
|
89
|
+
return ExporterDescriptor(
|
|
90
|
+
id="json", name="JSON Exporter", exporter=JsonExporter(),
|
|
91
|
+
attributes={FORMAT_ATTR: "json", EXTENSION_ATTR: ".json"},
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Ship it in its own package (with its own format dependencies, e.g. `h5py` for
|
|
96
|
+
HDF5). The consuming application decides which descriptors to register — there
|
|
97
|
+
is no implicit global discovery.
|
|
98
|
+
|
|
99
|
+
## Dependencies
|
|
100
|
+
|
|
101
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) — the tree protocols the exporter reads.
|
|
102
|
+
- [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the node payload type.
|
|
103
|
+
- [vcti-plugin-catalog](https://github.com/vcollab/vcti-python-plugin-catalog) — the `Descriptor` / `Registry` catalog.
|
|
104
|
+
|
|
105
|
+
(No heavy/runtime format dependencies live here — those belong to the per-format plugin packages.)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vcti-tree-exporter"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Format-agnostic exporters for vcti trees: an Exporter protocol and registry, with per-format writers as plugins."
|
|
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", "serialization", "exporter", "protocol", "registry", "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>=1.1.0",
|
|
29
|
+
"vcti-datanode>=2.0.0",
|
|
30
|
+
"vcti-plugin-catalog>=1.0.1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/vcollab/vcti-python-tree-exporter"
|
|
35
|
+
Repository = "https://github.com/vcollab/vcti-python-tree-exporter"
|
|
36
|
+
Changelog = "https://github.com/vcollab/vcti-python-tree-exporter/blob/main/CHANGELOG.md"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
include = ["vcti.treeexporter", "vcti.treeexporter.*"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
"vcti.treeexporter" = ["py.typed"]
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
test = ["pytest", "pytest-cov"]
|
|
47
|
+
lint = ["ruff"]
|
|
48
|
+
typecheck = ["mypy"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
zip-safe = true
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
addopts = "--cov=vcti.treeexporter --cov-report=term-missing --cov-fail-under=95"
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
python_version = "3.12"
|
|
58
|
+
strict = true
|
|
59
|
+
files = ["src"]
|
|
60
|
+
namespace_packages = true
|
|
61
|
+
explicit_package_bases = true
|
|
62
|
+
mypy_path = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.run]
|
|
65
|
+
branch = true
|
|
66
|
+
|
|
67
|
+
[tool.coverage.report]
|
|
68
|
+
exclude_also = [
|
|
69
|
+
"raise NotImplementedError",
|
|
70
|
+
"if TYPE_CHECKING:",
|
|
71
|
+
"if __name__ == .__main__.:",
|
|
72
|
+
"@(abc\\.)?abstractmethod",
|
|
73
|
+
"\\.\\.\\.",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
target-version = "py312"
|
|
78
|
+
line-length = 99
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.treeexporter — format-agnostic exporters for vcti trees.
|
|
4
|
+
|
|
5
|
+
Defines the :class:`Exporter` protocol — the contract a per-format writer
|
|
6
|
+
satisfies to serialize a ``vcti.tree`` tree of ``vcti.datanode.DataNode``
|
|
7
|
+
payloads — and the plugin-catalog integration for cataloguing exporters.
|
|
8
|
+
Concrete writers (HDF5, JSON, CSV, NumPy, …) ship as separate plugin packages,
|
|
9
|
+
each providing an :class:`ExporterDescriptor` through a
|
|
10
|
+
``get_exporter_descriptor()`` factory; a consumer registers the ones it needs
|
|
11
|
+
with :func:`build_registry` and resolves one by id with :func:`get_exporter` or
|
|
12
|
+
by attribute lookup over the registry.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
|
|
17
|
+
from vcti.plugincatalog import DuplicateEntryError, EntryNotFoundError, RegistryError
|
|
18
|
+
|
|
19
|
+
from .exceptions import ExportError, UnrepresentableNodeError
|
|
20
|
+
from .protocol import Exporter
|
|
21
|
+
from .registry import (
|
|
22
|
+
EXTENSION_ATTR,
|
|
23
|
+
FORMAT_ATTR,
|
|
24
|
+
ExporterDescriptor,
|
|
25
|
+
ExporterRegistry,
|
|
26
|
+
build_registry,
|
|
27
|
+
get_exporter,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = version("vcti-tree-exporter")
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"EXTENSION_ATTR",
|
|
34
|
+
"FORMAT_ATTR",
|
|
35
|
+
"DuplicateEntryError",
|
|
36
|
+
"EntryNotFoundError",
|
|
37
|
+
"ExportError",
|
|
38
|
+
"Exporter",
|
|
39
|
+
"ExporterDescriptor",
|
|
40
|
+
"ExporterRegistry",
|
|
41
|
+
"RegistryError",
|
|
42
|
+
"UnrepresentableNodeError",
|
|
43
|
+
"__version__",
|
|
44
|
+
"build_registry",
|
|
45
|
+
"get_exporter",
|
|
46
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Exceptions raised while exporting a tree.
|
|
4
|
+
|
|
5
|
+
These types are part of the :class:`~vcti.treeexporter.protocol.Exporter`
|
|
6
|
+
contract: a conforming exporter signals export-time failures with
|
|
7
|
+
:class:`ExportError` or a subclass, so a caller can ``except ExportError`` to
|
|
8
|
+
handle any exporter failure regardless of which format plugin produced it.
|
|
9
|
+
|
|
10
|
+
Built-in errors that are not export-specific are left as-is — e.g. ``export``
|
|
11
|
+
raises the standard ``FileExistsError`` when the destination already exists and
|
|
12
|
+
``overwrite`` is false.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExportError(Exception):
|
|
19
|
+
"""Base class for any failure while exporting a tree."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnrepresentableNodeError(ExportError):
|
|
23
|
+
"""A node's shape cannot be expressed in the target format.
|
|
24
|
+
|
|
25
|
+
Raised when an exporter meets a node the format has no way to represent —
|
|
26
|
+
for example an HDF5 group that would have to carry both child nodes and
|
|
27
|
+
array data.
|
|
28
|
+
"""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""The ``Exporter`` protocol — the contract every format writer satisfies.
|
|
4
|
+
|
|
5
|
+
An exporter serializes a tree to exactly one output format. The tree is any
|
|
6
|
+
``vcti.tree`` tree (e.g. ``DictTree``) whose node payloads are
|
|
7
|
+
``vcti.datanode.DataNode`` — a node's structural identity lives in
|
|
8
|
+
``node.intrinsic`` (notably ``name``), its free-form metadata in
|
|
9
|
+
``node.attributes``, and its optional array behind ``node.data`` /
|
|
10
|
+
``node.load()``. Concrete exporters live in separate per-format plugin packages
|
|
11
|
+
(HDF5, JSON, CSV, NumPy, …) and are catalogued as
|
|
12
|
+
:class:`~vcti.treeexporter.registry.ExporterDescriptor`.
|
|
13
|
+
|
|
14
|
+
The protocol is structural (PEP 544) — an exporter need only provide an
|
|
15
|
+
``export`` method; it does not have to inherit from anything. Format identity
|
|
16
|
+
and metadata (format name, default extension, …) are recorded on the exporter's
|
|
17
|
+
:class:`~vcti.treeexporter.registry.ExporterDescriptor`, not on the exporter
|
|
18
|
+
instance, so they are discoverable through the registry's attribute lookup.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from vcti.datanode import DataNode
|
|
28
|
+
from vcti.tree import ReadOnlyTree
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class Exporter(Protocol):
|
|
33
|
+
"""Serialize a tree of ``DataNode`` payloads to one output format.
|
|
34
|
+
|
|
35
|
+
An exporter is pure behavior: a single :meth:`export` method. Its format
|
|
36
|
+
name, default extension, and any other metadata are recorded on its
|
|
37
|
+
:class:`~vcti.treeexporter.registry.ExporterDescriptor` and resolved
|
|
38
|
+
through the registry — they are not read off the exporter instance.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def export(
|
|
42
|
+
self,
|
|
43
|
+
tree: ReadOnlyTree[DataNode, Any],
|
|
44
|
+
path: Path,
|
|
45
|
+
*,
|
|
46
|
+
overwrite: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Write ``tree`` to ``path``.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
tree: The tree to serialize (read-only access is sufficient).
|
|
52
|
+
path: Destination file or directory path.
|
|
53
|
+
overwrite: If ``False`` and ``path`` already exists, raise
|
|
54
|
+
``FileExistsError`` rather than replacing it.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
FileExistsError: ``path`` exists and ``overwrite`` is ``False``.
|
|
58
|
+
ExportError: The tree cannot be written — e.g.
|
|
59
|
+
:class:`~vcti.treeexporter.exceptions.UnrepresentableNodeError`
|
|
60
|
+
for a node the format cannot represent.
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Plugin-catalog integration for exporters.
|
|
4
|
+
|
|
5
|
+
Each exporter is catalogued as an :class:`ExporterDescriptor` — a
|
|
6
|
+
``vcti-plugin-catalog`` ``Descriptor`` whose ``instance`` is the
|
|
7
|
+
:class:`~vcti.treeexporter.protocol.Exporter` and whose ``attributes`` hold the
|
|
8
|
+
filterable metadata (format name, default extension, …). Descriptors are
|
|
9
|
+
collected in an :data:`ExporterRegistry`.
|
|
10
|
+
|
|
11
|
+
A consumer resolves an exporter either directly by id (:func:`get_exporter`) or
|
|
12
|
+
— the way that generalises to externally-sourced catalogs — by filtering the
|
|
13
|
+
registry's attribute lookup to find the matching id first::
|
|
14
|
+
|
|
15
|
+
from vcti.lookup import Rule
|
|
16
|
+
from vcti.treeexporter import EXTENSION_ATTR
|
|
17
|
+
|
|
18
|
+
(desc,) = registry.lookup.filter([Rule(EXTENSION_ATTR, "==", ".h5")])
|
|
19
|
+
desc.exporter.export(tree, path)
|
|
20
|
+
|
|
21
|
+
Exporter ids are a small, fixed, internally-controlled set, so using them
|
|
22
|
+
directly is fine; the attribute-lookup path is supported for consistency with
|
|
23
|
+
the rest of the ecosystem.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Iterable
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from vcti.plugincatalog import Descriptor, Registry
|
|
32
|
+
|
|
33
|
+
from .protocol import Exporter
|
|
34
|
+
|
|
35
|
+
#: Descriptor-attribute key holding an exporter's format name.
|
|
36
|
+
FORMAT_ATTR = "format"
|
|
37
|
+
|
|
38
|
+
#: Descriptor-attribute key holding an exporter's default output extension.
|
|
39
|
+
EXTENSION_ATTR = "extension"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExporterDescriptor(Descriptor[Exporter]):
|
|
43
|
+
"""A plugin-catalog descriptor whose ``instance`` is an :class:`Exporter`.
|
|
44
|
+
|
|
45
|
+
Mirrors the ``LoaderDescriptor`` shape: ``instance`` carries the exporter,
|
|
46
|
+
and ``attributes`` carry the filterable metadata (see :data:`FORMAT_ATTR`
|
|
47
|
+
and :data:`EXTENSION_ATTR`). :attr:`exporter` is a readable alias for
|
|
48
|
+
``instance``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
id: str,
|
|
54
|
+
name: str,
|
|
55
|
+
exporter: Exporter,
|
|
56
|
+
description: str | None = None,
|
|
57
|
+
attributes: dict[str, Any] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(
|
|
60
|
+
id=id,
|
|
61
|
+
name=name,
|
|
62
|
+
instance=exporter,
|
|
63
|
+
description=description,
|
|
64
|
+
attributes=attributes,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def exporter(self) -> Exporter:
|
|
69
|
+
"""The exporter instance."""
|
|
70
|
+
return self.instance
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"<ExporterDescriptor id={self.id!r} name={self.name!r}>"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#: A plugin-catalog registry of exporter descriptors.
|
|
77
|
+
ExporterRegistry = Registry[ExporterDescriptor]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_registry(descriptors: Iterable[ExporterDescriptor]) -> ExporterRegistry:
|
|
81
|
+
"""Build a registry from exporter descriptors.
|
|
82
|
+
|
|
83
|
+
Each plugin package provides its descriptor via a ``get_exporter_descriptor()``
|
|
84
|
+
factory; pass those here. Raises ``vcti.plugincatalog.DuplicateEntryError``
|
|
85
|
+
if two descriptors share an id.
|
|
86
|
+
"""
|
|
87
|
+
# plugin-catalog's Registry.__init__ lacks a `-> None` annotation, which
|
|
88
|
+
# trips mypy's strict no-untyped-call; safe to ignore here.
|
|
89
|
+
registry: ExporterRegistry = ExporterRegistry() # type: ignore[no-untyped-call]
|
|
90
|
+
registry.register_many(descriptors)
|
|
91
|
+
return registry
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_exporter(registry: ExporterRegistry, exporter_id: str) -> Exporter:
|
|
95
|
+
"""Return the exporter registered under ``exporter_id``.
|
|
96
|
+
|
|
97
|
+
Raises ``vcti.plugincatalog.EntryNotFoundError`` if no descriptor matches.
|
|
98
|
+
"""
|
|
99
|
+
return registry.get(exporter_id).exporter
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-tree-exporter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Format-agnostic exporters for vcti trees: an Exporter protocol and registry, with per-format writers as plugins.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/vcollab/vcti-python-tree-exporter
|
|
8
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-tree-exporter
|
|
9
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-tree-exporter/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: tree,export,serialization,exporter,protocol,registry,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>=1.1.0
|
|
23
|
+
Requires-Dist: vcti-datanode>=2.0.0
|
|
24
|
+
Requires-Dist: vcti-plugin-catalog>=1.0.1
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
28
|
+
Provides-Extra: lint
|
|
29
|
+
Requires-Dist: ruff; extra == "lint"
|
|
30
|
+
Provides-Extra: typecheck
|
|
31
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Tree Exporter
|
|
35
|
+
|
|
36
|
+
Format-agnostic exporters for vcti trees: an `Exporter` protocol and a plugin catalog, with per-format writers as plugins.
|
|
37
|
+
|
|
38
|
+
`vcti-tree-exporter` defines *what an exporter is* and *how to catalogue and
|
|
39
|
+
look one up*. It implements no file format itself — concrete writers (HDF5,
|
|
40
|
+
JSON, CSV, NumPy, …) live in separate plugin packages and are registered by the
|
|
41
|
+
consumer. The thing being exported is any
|
|
42
|
+
[`vcti-tree`](https://github.com/vcollab/vcti-python-tree) tree whose node
|
|
43
|
+
payloads are [`vcti-datanode`](https://github.com/vcollab/vcti-python-datanode)
|
|
44
|
+
`DataNode`s (data + attributes), so one tree can be written to any registered
|
|
45
|
+
format without the producer knowing about serialization.
|
|
46
|
+
|
|
47
|
+
Cataloguing and lookup are built on
|
|
48
|
+
[`vcti-plugin-catalog`](https://github.com/vcollab/vcti-python-plugin-catalog)
|
|
49
|
+
(`Descriptor` + `Registry`).
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install vcti-tree-exporter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The base package is intentionally small. Install a format plugin for each
|
|
58
|
+
format you need:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install vcti-tree-exporter-hdf5 # single .h5 file
|
|
62
|
+
pip install vcti-tree-exporter-json # single .json document
|
|
63
|
+
pip install vcti-tree-exporter-npz # single .npz archive
|
|
64
|
+
pip install vcti-tree-exporter-csv # directory tree of CSV tables
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Core concepts
|
|
68
|
+
|
|
69
|
+
| Piece | Role |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `Exporter` | Structural protocol — a single `export(tree, path, *, overwrite=False)` method. Pure behavior; format name and extension are **not** on the exporter. |
|
|
72
|
+
| `ExporterDescriptor` | A `vcti-plugin-catalog` `Descriptor[Exporter]`: the exporter is its `instance`; `format`, `extension`, and any other metadata are filterable `attributes`. |
|
|
73
|
+
| `get_exporter_descriptor()` | Factory each plugin package provides, returning its `ExporterDescriptor`. |
|
|
74
|
+
| `build_registry(descriptors)` | Build a plugin-catalog `Registry` from those descriptors. |
|
|
75
|
+
| `get_exporter(registry, id)` | Resolve an exporter instance by descriptor id. |
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
Register the descriptors your application uses, then resolve one and write a
|
|
80
|
+
tree:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
from vcti.treeexporter import build_registry, get_exporter
|
|
85
|
+
from vcti.treeexporter_hdf5 import get_exporter_descriptor as hdf5_descriptor
|
|
86
|
+
from vcti.treeexporter_json import get_exporter_descriptor as json_descriptor
|
|
87
|
+
|
|
88
|
+
registry = build_registry([hdf5_descriptor(), json_descriptor()])
|
|
89
|
+
get_exporter(registry, "hdf5").export(tree, Path("model.h5"), overwrite=True)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`registry` is an ordinary `vcti.plugincatalog.Registry`, so the general way to
|
|
93
|
+
select an exporter is to filter by attributes, then use the matching id — the
|
|
94
|
+
same pattern as the rest of the ecosystem:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from vcti.lookup import Rule
|
|
98
|
+
from vcti.treeexporter import EXTENSION_ATTR
|
|
99
|
+
|
|
100
|
+
(desc,) = registry.lookup.filter([Rule(EXTENSION_ATTR, "==", ".h5")])
|
|
101
|
+
desc.exporter.export(tree, Path("model.h5"))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Exporter ids are a small, fixed, well-known set, so using `get_exporter(registry,
|
|
105
|
+
"hdf5")` directly is also fine.
|
|
106
|
+
|
|
107
|
+
## Writing a format plugin
|
|
108
|
+
|
|
109
|
+
A plugin's exporter only has to satisfy the protocol — one `export` method, no
|
|
110
|
+
base class — and ship a `get_exporter_descriptor()` factory carrying its
|
|
111
|
+
metadata:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
from vcti.treeexporter import EXTENSION_ATTR, FORMAT_ATTR, ExporterDescriptor
|
|
116
|
+
|
|
117
|
+
class JsonExporter:
|
|
118
|
+
def export(self, tree, path: Path, *, overwrite: bool = False) -> None:
|
|
119
|
+
... # walk the tree (node.name, node.attributes, node.load()), write JSON
|
|
120
|
+
|
|
121
|
+
def get_exporter_descriptor() -> ExporterDescriptor:
|
|
122
|
+
return ExporterDescriptor(
|
|
123
|
+
id="json", name="JSON Exporter", exporter=JsonExporter(),
|
|
124
|
+
attributes={FORMAT_ATTR: "json", EXTENSION_ATTR: ".json"},
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Ship it in its own package (with its own format dependencies, e.g. `h5py` for
|
|
129
|
+
HDF5). The consuming application decides which descriptors to register — there
|
|
130
|
+
is no implicit global discovery.
|
|
131
|
+
|
|
132
|
+
## Dependencies
|
|
133
|
+
|
|
134
|
+
- [vcti-tree](https://github.com/vcollab/vcti-python-tree) — the tree protocols the exporter reads.
|
|
135
|
+
- [vcti-datanode](https://github.com/vcollab/vcti-python-datanode) — the node payload type.
|
|
136
|
+
- [vcti-plugin-catalog](https://github.com/vcollab/vcti-python-plugin-catalog) — the `Descriptor` / `Registry` catalog.
|
|
137
|
+
|
|
138
|
+
(No heavy/runtime format dependencies live here — those belong to the per-format plugin packages.)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/vcti/treeexporter/__init__.py
|
|
5
|
+
src/vcti/treeexporter/exceptions.py
|
|
6
|
+
src/vcti/treeexporter/protocol.py
|
|
7
|
+
src/vcti/treeexporter/py.typed
|
|
8
|
+
src/vcti/treeexporter/registry.py
|
|
9
|
+
src/vcti_tree_exporter.egg-info/PKG-INFO
|
|
10
|
+
src/vcti_tree_exporter.egg-info/SOURCES.txt
|
|
11
|
+
src/vcti_tree_exporter.egg-info/dependency_links.txt
|
|
12
|
+
src/vcti_tree_exporter.egg-info/requires.txt
|
|
13
|
+
src/vcti_tree_exporter.egg-info/top_level.txt
|
|
14
|
+
src/vcti_tree_exporter.egg-info/zip-safe
|
|
15
|
+
tests/test_registry.py
|
|
16
|
+
tests/test_version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Tests for the Exporter protocol and the plugin-catalog integration."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from vcti.lookup import Rule
|
|
11
|
+
from vcti.plugincatalog import DuplicateEntryError, EntryNotFoundError
|
|
12
|
+
|
|
13
|
+
from vcti.treeexporter import (
|
|
14
|
+
EXTENSION_ATTR,
|
|
15
|
+
FORMAT_ATTR,
|
|
16
|
+
Exporter,
|
|
17
|
+
ExporterDescriptor,
|
|
18
|
+
ExportError,
|
|
19
|
+
UnrepresentableNodeError,
|
|
20
|
+
build_registry,
|
|
21
|
+
get_exporter,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _FakeExporter:
|
|
26
|
+
"""A minimal structural Exporter for testing — pure behavior."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.calls: list[tuple] = []
|
|
30
|
+
|
|
31
|
+
def export(self, tree, path, *, overwrite: bool = False) -> None:
|
|
32
|
+
self.calls.append((tree, Path(path), overwrite))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _descriptor(exporter_id: str, fmt: str, extension: str, **attrs: object) -> ExporterDescriptor:
|
|
36
|
+
return ExporterDescriptor(
|
|
37
|
+
id=exporter_id,
|
|
38
|
+
name=exporter_id,
|
|
39
|
+
exporter=_FakeExporter(),
|
|
40
|
+
attributes={FORMAT_ATTR: fmt, EXTENSION_ATTR: extension, **attrs},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_fake_satisfies_protocol():
|
|
45
|
+
assert isinstance(_FakeExporter(), Exporter)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_object_missing_export_fails_protocol():
|
|
49
|
+
class NotAnExporter:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
assert not isinstance(NotAnExporter(), Exporter)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_descriptor_exposes_exporter_and_attributes():
|
|
56
|
+
exporter = _FakeExporter()
|
|
57
|
+
desc = ExporterDescriptor(
|
|
58
|
+
id="hdf5",
|
|
59
|
+
name="HDF5",
|
|
60
|
+
exporter=exporter,
|
|
61
|
+
description="HDF5 writer",
|
|
62
|
+
attributes={FORMAT_ATTR: "hdf5", EXTENSION_ATTR: ".h5", "library": "h5py"},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert desc.id == "hdf5"
|
|
66
|
+
assert desc.name == "HDF5"
|
|
67
|
+
assert desc.description == "HDF5 writer"
|
|
68
|
+
assert desc.exporter is exporter
|
|
69
|
+
assert desc.instance is exporter
|
|
70
|
+
assert desc.attributes[FORMAT_ATTR] == "hdf5"
|
|
71
|
+
assert desc.attributes[EXTENSION_ATTR] == ".h5"
|
|
72
|
+
assert desc.attributes["library"] == "h5py"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_descriptor_repr_includes_id():
|
|
76
|
+
assert "hdf5" in repr(_descriptor("hdf5", "hdf5", ".h5"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_build_registry_and_resolve_by_id():
|
|
80
|
+
registry = build_registry(
|
|
81
|
+
[_descriptor("json", "json", ".json"), _descriptor("csv", "csv", "")]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert set(registry.ids()) == {"json", "csv"}
|
|
85
|
+
assert get_exporter(registry, "json") is registry.get("json").exporter
|
|
86
|
+
assert registry.get("csv").attributes[EXTENSION_ATTR] == ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_lookup_by_extension_attribute():
|
|
90
|
+
registry = build_registry(
|
|
91
|
+
[_descriptor("hdf5", "hdf5", ".h5"), _descriptor("json", "json", ".json")]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
matches = registry.lookup.filter([Rule(EXTENSION_ATTR, "==", ".h5")])
|
|
95
|
+
assert [d.id for d in matches] == ["hdf5"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_lookup_by_format_attribute_then_use_id():
|
|
99
|
+
registry = build_registry([_descriptor("hdf5", "hdf5", ".h5")])
|
|
100
|
+
|
|
101
|
+
(match,) = registry.lookup.filter([Rule(FORMAT_ATTR, "==", "hdf5")])
|
|
102
|
+
assert get_exporter(registry, match.id) is match.exporter
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_get_unknown_id_raises():
|
|
106
|
+
registry = build_registry([_descriptor("json", "json", ".json")])
|
|
107
|
+
with pytest.raises(EntryNotFoundError):
|
|
108
|
+
get_exporter(registry, "nope")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_duplicate_id_raises():
|
|
112
|
+
with pytest.raises(DuplicateEntryError):
|
|
113
|
+
build_registry(
|
|
114
|
+
[_descriptor("json", "json", ".json"), _descriptor("json", "json", ".json")]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_exception_hierarchy():
|
|
119
|
+
assert issubclass(UnrepresentableNodeError, ExportError)
|
|
120
|
+
assert issubclass(ExportError, Exception)
|
|
121
|
+
# not a ValueError — callers catch ExportError, not a broad builtin
|
|
122
|
+
assert not issubclass(ExportError, ValueError)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_export_signature_round_trips():
|
|
126
|
+
exporter = _FakeExporter()
|
|
127
|
+
exporter.export("TREE", Path("out.json"), overwrite=True)
|
|
128
|
+
assert exporter.calls == [("TREE", Path("out.json"), True)]
|
|
@@ -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."""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import vcti.treeexporter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestVersion:
|
|
11
|
+
def test_version_exists(self):
|
|
12
|
+
assert hasattr(vcti.treeexporter, "__version__")
|
|
13
|
+
|
|
14
|
+
def test_version_is_valid_semver(self):
|
|
15
|
+
assert re.match(r"^\d+\.\d+\.\d+", vcti.treeexporter.__version__)
|