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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ...
@@ -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,13 @@
1
+ vcti-tree>=1.1.0
2
+ vcti-datanode>=2.0.0
3
+ vcti-plugin-catalog>=1.0.1
4
+
5
+ [lint]
6
+ ruff
7
+
8
+ [test]
9
+ pytest
10
+ pytest-cov
11
+
12
+ [typecheck]
13
+ mypy
@@ -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__)