dstrack 0.1.0__py3-none-any.whl
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.
- dstrack/__init__.py +8 -0
- dstrack/_benchmark/__init__.py +54 -0
- dstrack/_benchmark/_cli.py +114 -0
- dstrack/_benchmark/_environment.py +144 -0
- dstrack/_benchmark/_profiling.py +207 -0
- dstrack/_benchmark/_report.py +152 -0
- dstrack/_benchmark/_runner.py +196 -0
- dstrack/_benchmark/_synthetic.py +158 -0
- dstrack/_cli.py +105 -0
- dstrack/_log.py +268 -0
- dstrack/_log_render.py +282 -0
- dstrack/_logging.py +60 -0
- dstrack/_store_template.py +58 -0
- dstrack/_track.py +144 -0
- dstrack/cache.py +448 -0
- dstrack/console.py +47 -0
- dstrack/errors.py +22 -0
- dstrack/paths.py +88 -0
- dstrack/readers/__init__.py +69 -0
- dstrack/readers/_csv.py +361 -0
- dstrack/readers/_protocol.py +120 -0
- dstrack/readers/_registry.py +241 -0
- dstrack/readers/_resolve.py +140 -0
- dstrack/schemas/snapshot_v1.json +299 -0
- dstrack/snapshot/__init__.py +46 -0
- dstrack/snapshot/_builder.py +114 -0
- dstrack/snapshot/_metadata.py +154 -0
- dstrack/snapshot/_stats.py +487 -0
- dstrack/snapshot/_version.py +13 -0
- dstrack/store.py +765 -0
- dstrack/utils.py +6 -0
- dstrack-0.1.0.dist-info/METADATA +123 -0
- dstrack-0.1.0.dist-info/RECORD +36 -0
- dstrack-0.1.0.dist-info/WHEEL +4 -0
- dstrack-0.1.0.dist-info/entry_points.txt +4 -0
- dstrack-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Registry of reader classes, keyed by short name and by file extension.
|
|
2
|
+
|
|
3
|
+
Registration is the single extension point for readers. Three kinds of user
|
|
4
|
+
reach it by three different routes, and none of them should have to do the other
|
|
5
|
+
two's work:
|
|
6
|
+
|
|
7
|
+
- **From Python**, a caller who already has a reader instance hands it straight
|
|
8
|
+
to [SnapshotBuilder][dstrack.snapshot._builder.SnapshotBuilder]; the registry
|
|
9
|
+
is never consulted.
|
|
10
|
+
- **From an installed plugin package**, a reader self-registers through the
|
|
11
|
+
``dstrack.readers`` entry-point group, so ``dstrack track data.parquet`` infers
|
|
12
|
+
it from the extension with nothing to type.
|
|
13
|
+
- **From an ad-hoc class** in the user's own project, which is not installed as a
|
|
14
|
+
plugin and so is named explicitly as ``"package.module:ClassName"`` (see
|
|
15
|
+
[dstrack.readers._resolve][]).
|
|
16
|
+
|
|
17
|
+
A registered class must satisfy both
|
|
18
|
+
[TabularReader][dstrack.readers._protocol.TabularReader] (how it reads) and
|
|
19
|
+
[ReaderFactory][dstrack.readers._protocol.ReaderFactory] (how it is built from a
|
|
20
|
+
path);
|
|
21
|
+
[check_reader_class()][dstrack.readers._registry.check_reader_class] enforces
|
|
22
|
+
that *before* the class is ever instantiated.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import inspect
|
|
26
|
+
import logging
|
|
27
|
+
from collections.abc import Sequence
|
|
28
|
+
from importlib.metadata import entry_points
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Final
|
|
31
|
+
|
|
32
|
+
from ._csv import CsvReader
|
|
33
|
+
from ._protocol import ReaderFactory, TabularReader
|
|
34
|
+
|
|
35
|
+
_log = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Entry-point group an installed package declares to add a reader, e.g.
|
|
38
|
+
# [project.entry-points."dstrack.readers"]
|
|
39
|
+
# parquet = "dstrack_parquet:ParquetReader"
|
|
40
|
+
ENTRY_POINT_GROUP: Final[str] = "dstrack.readers"
|
|
41
|
+
|
|
42
|
+
# Short name (as typed for --reader) -> reader class.
|
|
43
|
+
_BY_NAME: dict[str, type[TabularReader]] = {}
|
|
44
|
+
# Lowercase file extension, leading dot included -> reader class.
|
|
45
|
+
_BY_EXTENSION: dict[str, type[TabularReader]] = {}
|
|
46
|
+
|
|
47
|
+
_plugins_loaded = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_reader_class(obj: object, *, origin: str) -> type[TabularReader]:
|
|
51
|
+
"""Validate that ``obj`` is a class usable as a reader, without calling it.
|
|
52
|
+
|
|
53
|
+
Both protocols are checked here, on the class, so that a class which merely
|
|
54
|
+
*looks* like a reader is rejected before its constructor runs on a user path.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
obj: The candidate, typically freshly imported or freshly registered.
|
|
58
|
+
origin: How the caller referred to it (a spec, a name, an entry point),
|
|
59
|
+
used to make the error message actionable.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
``obj`` itself, narrowed to a reader class.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
TypeError: If ``obj`` is not a class, or does not satisfy
|
|
66
|
+
[TabularReader][dstrack.readers._protocol.TabularReader] and
|
|
67
|
+
[ReaderFactory][dstrack.readers._protocol.ReaderFactory].
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(obj, type):
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f"{origin} resolved to {obj!r}, which is not a class. "
|
|
72
|
+
"A reader must be a class, not an instance or a function."
|
|
73
|
+
)
|
|
74
|
+
qualname = f"{obj.__module__}.{obj.__qualname__}"
|
|
75
|
+
if not issubclass(obj, TabularReader):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
f"{qualname} (from {origin}) does not satisfy the TabularReader "
|
|
78
|
+
"protocol: it must define columns() and iter_batches()."
|
|
79
|
+
)
|
|
80
|
+
# Checked through an object-typed name: mypy assumes any class object
|
|
81
|
+
# already satisfies ReaderFactory, so narrowing from `object` is what keeps
|
|
82
|
+
# this branch live for the type checker as well as at runtime.
|
|
83
|
+
candidate: object = obj
|
|
84
|
+
if not isinstance(candidate, ReaderFactory):
|
|
85
|
+
raise TypeError(
|
|
86
|
+
f"{qualname} (from {origin}) does not satisfy the ReaderFactory "
|
|
87
|
+
"protocol: it must define a from_path(path) classmethod, which is "
|
|
88
|
+
"how dstrack builds a reader it knows only by name."
|
|
89
|
+
)
|
|
90
|
+
# Check that from_path is a class method
|
|
91
|
+
if not isinstance(inspect.getattr_static(obj, "from_path"), classmethod):
|
|
92
|
+
raise TypeError(
|
|
93
|
+
f"{qualname} (from {origin}) defines from_path, but not as a "
|
|
94
|
+
"classmethod: dstrack calls it on the class (MyReader.from_path(path)), "
|
|
95
|
+
"so it must be decorated with @classmethod."
|
|
96
|
+
)
|
|
97
|
+
return obj
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_reader(reader_cls: type[TabularReader], path: Path) -> TabularReader:
|
|
101
|
+
"""Instantiate a validated reader class for ``path``.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
reader_cls: A class already validated through
|
|
105
|
+
[check_reader_class()][dstrack.readers._registry.check_reader_class].
|
|
106
|
+
path: Path to the dataset source.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The reader instance.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
TypeError: If ``reader_cls`` was never validated and lacks ``from_path``,
|
|
113
|
+
or if its ``from_path`` returns something that is not a
|
|
114
|
+
[TabularReader][dstrack.readers._protocol.TabularReader].
|
|
115
|
+
"""
|
|
116
|
+
factory: object = reader_cls
|
|
117
|
+
if not isinstance(factory, ReaderFactory):
|
|
118
|
+
raise TypeError(f"{reader_cls!r} is not an instance of {ReaderFactory}")
|
|
119
|
+
reader: object = factory.from_path(path)
|
|
120
|
+
if not isinstance(reader, TabularReader):
|
|
121
|
+
qualname = f"{reader_cls.__module__}.{reader_cls.__qualname__}"
|
|
122
|
+
raise TypeError(
|
|
123
|
+
f"{qualname}.from_path returned {reader!r}, which is not a "
|
|
124
|
+
"TabularReader: from_path must build an object that follows this protocol."
|
|
125
|
+
)
|
|
126
|
+
return reader
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def register_reader(
|
|
130
|
+
reader_cls: type[TabularReader],
|
|
131
|
+
*,
|
|
132
|
+
name: str,
|
|
133
|
+
extensions: Sequence[str] | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Register a reader under a short name and the extensions it handles.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
reader_cls: The reader class. Must satisfy both
|
|
139
|
+
[TabularReader][dstrack.readers._protocol.TabularReader] and
|
|
140
|
+
[ReaderFactory][dstrack.readers._protocol.ReaderFactory].
|
|
141
|
+
name: Short name, as typed for ``--reader`` (e.g. ``"csv"``).
|
|
142
|
+
extensions: Extensions this reader claims, leading dot included. When
|
|
143
|
+
omitted, the class's ``EXTENSIONS`` attribute is used, so a plugin
|
|
144
|
+
can declare its extensions once on the class and register through a
|
|
145
|
+
bare entry point.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
TypeError: If ``reader_cls`` does not satisfy both reader protocols.
|
|
149
|
+
ValueError: If ``name`` or any extension is already taken. Registration
|
|
150
|
+
never silently displaces an existing reader: two packages fighting
|
|
151
|
+
over ``.parquet`` is a conflict the user has to see, not one to
|
|
152
|
+
resolve by import order.
|
|
153
|
+
ValueError: If any extension is malformed. An extension is matched
|
|
154
|
+
against [Path.suffix][pathlib.PurePath.suffix], so it must be a
|
|
155
|
+
single leading-dot suffix such as ``".csv"``: a value like ``"csv"``
|
|
156
|
+
or ``".tar.gz"`` could never match and is rejected rather than
|
|
157
|
+
silently registered as dead weight.
|
|
158
|
+
"""
|
|
159
|
+
check_reader_class(reader_cls, origin=f"reader {name!r}")
|
|
160
|
+
|
|
161
|
+
if extensions is None:
|
|
162
|
+
extensions = getattr(reader_cls, "EXTENSIONS", ())
|
|
163
|
+
|
|
164
|
+
if name in _BY_NAME:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Reader name {name!r} is already registered to "
|
|
167
|
+
f"{_BY_NAME[name].__module__}.{_BY_NAME[name].__qualname__}."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
claimed = [ext.lower() for ext in extensions]
|
|
171
|
+
for ext in claimed:
|
|
172
|
+
# An extension is only ever compared against Path.suffix, which is a
|
|
173
|
+
# single leading-dot component (".csv").
|
|
174
|
+
if not ext.startswith(".") or ext == "." or Path(f"_{ext}").suffix != ext:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Extension {ext!r} (for reader {name!r}) is malformed: an "
|
|
177
|
+
"extension must be a single leading-dot suffix such as '.csv', "
|
|
178
|
+
"so that it can match Path.suffix."
|
|
179
|
+
)
|
|
180
|
+
for ext in claimed:
|
|
181
|
+
if ext in _BY_EXTENSION:
|
|
182
|
+
owner = _BY_EXTENSION[ext]
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Extension {ext!r} is already registered to "
|
|
185
|
+
f"{owner.__module__}.{owner.__qualname__}."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
_BY_NAME[name] = reader_cls
|
|
189
|
+
for ext in claimed:
|
|
190
|
+
_BY_EXTENSION[ext] = reader_cls
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _load_plugins() -> None:
|
|
194
|
+
"""Register readers advertised by installed packages, once per process.
|
|
195
|
+
|
|
196
|
+
A plugin that fails to import or conflicts with an existing registration is
|
|
197
|
+
logged and skipped: one broken third-party package must not take down
|
|
198
|
+
`dstrack track` for a dataset that does not even use it.
|
|
199
|
+
"""
|
|
200
|
+
global _plugins_loaded
|
|
201
|
+
if _plugins_loaded:
|
|
202
|
+
return
|
|
203
|
+
# Set before loading: a plugin that raises must not be retried on every
|
|
204
|
+
# subsequent lookup.
|
|
205
|
+
_plugins_loaded = True
|
|
206
|
+
|
|
207
|
+
for entry_point in entry_points(group=ENTRY_POINT_GROUP):
|
|
208
|
+
try:
|
|
209
|
+
register_reader(entry_point.load(), name=entry_point.name)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
_log.warning(
|
|
212
|
+
f"Ignoring reader plugin {entry_point.name!r} "
|
|
213
|
+
f"({entry_point.value}): {e}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def reader_class_for_name(name: str) -> type[TabularReader] | None:
|
|
218
|
+
"""Look up a reader by its short name, or ``None`` if nothing claims it."""
|
|
219
|
+
_load_plugins()
|
|
220
|
+
return _BY_NAME.get(name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def reader_class_for_extension(extension: str) -> type[TabularReader] | None:
|
|
224
|
+
"""Look up a reader by file extension, or ``None`` if nothing claims it."""
|
|
225
|
+
_load_plugins()
|
|
226
|
+
return _BY_EXTENSION.get(extension.lower())
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def available_readers() -> dict[str, type[TabularReader]]:
|
|
230
|
+
"""Return all registered readers by short name, built-in and plugin alike."""
|
|
231
|
+
_load_plugins()
|
|
232
|
+
return dict(_BY_NAME)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def known_extensions() -> dict[str, type[TabularReader]]:
|
|
236
|
+
"""Return all registered extensions and the reader class that claims each."""
|
|
237
|
+
_load_plugins()
|
|
238
|
+
return dict(_BY_EXTENSION)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
register_reader(CsvReader, name="csv")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Selection and loading of a reader for a given source.
|
|
2
|
+
|
|
3
|
+
A [TabularReader][dstrack.readers._protocol.TabularReader] is chosen one of three
|
|
4
|
+
ways, in order of how much the user has to type:
|
|
5
|
+
|
|
6
|
+
1. Implicitly, from the source file's extension (``data.csv`` ->
|
|
7
|
+
[CsvReader][dstrack.readers._csv.CsvReader]).
|
|
8
|
+
Plugin readers registered through the ``dstrack.readers`` entry-point group
|
|
9
|
+
participate here too, so an installed reader needs nothing on the command line.
|
|
10
|
+
2. By short name (``"csv"``), for when the extension is absent or misleading.
|
|
11
|
+
3. By ``"package.module:ClassName"`` spec, which imports the class directly. This
|
|
12
|
+
is the escape hatch for a reader that lives in the user's own project and is
|
|
13
|
+
not installed as a plugin.
|
|
14
|
+
|
|
15
|
+
Security: a spec is arbitrary import-by-name, i.e. code execution. It is only
|
|
16
|
+
ever accepted from the invoking user (a ``--reader`` argument they typed), and
|
|
17
|
+
is never persisted into a snapshot nor read back out of one. See ADR-0003.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ._protocol import TabularReader
|
|
24
|
+
from ._registry import (
|
|
25
|
+
available_readers,
|
|
26
|
+
build_reader,
|
|
27
|
+
check_reader_class,
|
|
28
|
+
known_extensions,
|
|
29
|
+
reader_class_for_extension,
|
|
30
|
+
reader_class_for_name,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_reader_class(spec: str) -> type[TabularReader]:
|
|
35
|
+
"""Import a reader class from a ``"package.module:ClassName"`` spec.
|
|
36
|
+
|
|
37
|
+
The class is validated against both reader protocols before being returned,
|
|
38
|
+
so a mistyped spec that happens to name some other importable object fails
|
|
39
|
+
here rather than part-way through a snapshot.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
spec: A ``"<module>:<class>"`` string. The module part is imported and
|
|
43
|
+
the class part is looked up on it.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The referenced class (not an instance).
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If ``spec`` is not of the form ``"module:ClassName"``, the
|
|
50
|
+
module cannot be imported, or the class is not found on it.
|
|
51
|
+
TypeError: If the referenced object is not a usable reader class.
|
|
52
|
+
"""
|
|
53
|
+
module_name, sep, class_name = spec.partition(":")
|
|
54
|
+
if not sep or not module_name or not class_name:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Invalid reader spec {spec!r}. "
|
|
57
|
+
"Expected the form 'package.module:ClassName'."
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
module = importlib.import_module(module_name)
|
|
61
|
+
except ImportError as e:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Could not import module {module_name!r} from reader spec {spec!r}."
|
|
64
|
+
) from e
|
|
65
|
+
try:
|
|
66
|
+
obj = getattr(module, class_name)
|
|
67
|
+
except AttributeError as e:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Module {module_name!r} has no attribute {class_name!r} "
|
|
70
|
+
f"(from reader spec {spec!r})."
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
return check_reader_class(obj, origin=f"reader spec {spec!r}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_reader_class(
|
|
77
|
+
path: str | Path, *, reader: str | None = None
|
|
78
|
+
) -> type[TabularReader]:
|
|
79
|
+
"""Choose the reader class for ``path``, without instantiating it.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Path to the dataset source.
|
|
83
|
+
reader: Either a registered short name (``"csv"``) or a
|
|
84
|
+
``"package.module:ClassName"`` spec, told apart by the ``":"``.
|
|
85
|
+
When omitted, the reader is inferred from ``path``'s extension.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A validated reader class.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If ``reader`` names nothing known, a spec is malformed, or
|
|
92
|
+
no reader is registered for ``path``'s extension.
|
|
93
|
+
TypeError: If the resolved class is not a usable reader.
|
|
94
|
+
"""
|
|
95
|
+
if reader is not None:
|
|
96
|
+
if ":" in reader:
|
|
97
|
+
return load_reader_class(reader)
|
|
98
|
+
reader_cls = reader_class_for_name(reader)
|
|
99
|
+
if reader_cls is None:
|
|
100
|
+
known = ", ".join(sorted(available_readers())) or "(none)"
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Unknown reader {reader!r}. Available readers: {known}. "
|
|
103
|
+
"Pass 'package.module:ClassName' to use a reader that is not "
|
|
104
|
+
"installed as a plugin."
|
|
105
|
+
)
|
|
106
|
+
return reader_cls
|
|
107
|
+
|
|
108
|
+
path = Path(path)
|
|
109
|
+
extension = path.suffix.lower()
|
|
110
|
+
reader_cls = reader_class_for_extension(extension)
|
|
111
|
+
if reader_cls is None:
|
|
112
|
+
known = ", ".join(sorted(known_extensions())) or "(none)"
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"No reader registered for extension {extension!r} (path: {path}). "
|
|
115
|
+
f"Known extensions: {known}. Name a reader explicitly, either a "
|
|
116
|
+
"registered one or 'package.module:ClassName'."
|
|
117
|
+
)
|
|
118
|
+
return reader_cls
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resolve_reader(path: str | Path, *, reader: str | None = None) -> TabularReader:
|
|
122
|
+
"""Build a reader for ``path``, explicitly named or inferred from its extension.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
path: Path to the dataset source the reader will read.
|
|
126
|
+
reader: Optional short name or ``"package.module:ClassName"`` spec. When
|
|
127
|
+
omitted, the reader is inferred from ``path``'s file extension.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
An instantiated reader satisfying
|
|
131
|
+
[TabularReader][dstrack.readers._protocol.TabularReader].
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If ``reader`` names nothing known, a spec is malformed, or
|
|
135
|
+
no reader is registered for ``path``'s extension.
|
|
136
|
+
TypeError: If the resolved class is not a usable reader.
|
|
137
|
+
"""
|
|
138
|
+
path = Path(path)
|
|
139
|
+
reader_cls = resolve_reader_class(path, reader=reader)
|
|
140
|
+
return build_reader(reader_cls, path)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/leoyala/dstrack/schemas/snapshot_v1.json",
|
|
4
|
+
"title": "DatasetSnapshot",
|
|
5
|
+
"description": "A dstrack dataset snapshot. Captures semantic information about a tabular dataset at a point in time. See ADR-0001.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"format_version",
|
|
9
|
+
"snapshot_id",
|
|
10
|
+
"created_at",
|
|
11
|
+
"created_by",
|
|
12
|
+
"dataset_name",
|
|
13
|
+
"dataset_path",
|
|
14
|
+
"source_type",
|
|
15
|
+
"source_hash",
|
|
16
|
+
"num_columns",
|
|
17
|
+
"columns",
|
|
18
|
+
"schema_hash",
|
|
19
|
+
"num_rows",
|
|
20
|
+
"column_stats",
|
|
21
|
+
"duplicate_row_fraction",
|
|
22
|
+
"near_duplicate_estimate",
|
|
23
|
+
"constant_columns",
|
|
24
|
+
"high_null_columns",
|
|
25
|
+
"row_minhash",
|
|
26
|
+
"row_hyperloglog"
|
|
27
|
+
],
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"properties": {
|
|
30
|
+
"format_version": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"const": "1",
|
|
33
|
+
"description": "Schema version. Readers must reject snapshots with an unrecognised version."
|
|
34
|
+
},
|
|
35
|
+
"snapshot_id": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"format": "uuid",
|
|
38
|
+
"description": "Unique identifier for this snapshot."
|
|
39
|
+
},
|
|
40
|
+
"created_at": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"format": "date-time",
|
|
43
|
+
"description": "ISO-8601 timestamp of when the snapshot was taken."
|
|
44
|
+
},
|
|
45
|
+
"created_by": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "User or process that created the snapshot."
|
|
48
|
+
},
|
|
49
|
+
"dataset_name": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Human-readable name for the dataset."
|
|
52
|
+
},
|
|
53
|
+
"dataset_path": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Source path or URI at snapshot time."
|
|
56
|
+
},
|
|
57
|
+
"source_type": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"examples": ["file", "directory", "database", "huggingface", "s3"],
|
|
60
|
+
"description": "Origin kind of the dataset source."
|
|
61
|
+
},
|
|
62
|
+
"source_hash": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Cryptographic hash of the raw source bytes, or a Merkle tree hash for directories."
|
|
65
|
+
},
|
|
66
|
+
"description": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Free-text description of what this dataset represents."
|
|
69
|
+
},
|
|
70
|
+
"tags": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": { "type": "string" },
|
|
73
|
+
"description": "Arbitrary key-value labels for filtering and organisation."
|
|
74
|
+
},
|
|
75
|
+
"num_columns": {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"minimum": 0,
|
|
78
|
+
"description": "Total number of columns/features."
|
|
79
|
+
},
|
|
80
|
+
"columns": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"items": { "$ref": "#/$defs/ColumnDescriptor" },
|
|
83
|
+
"description": "Ordered list of column descriptors."
|
|
84
|
+
},
|
|
85
|
+
"schema_hash": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Hash of (name, dtype) tuples in column order. Changes whenever the schema changes."
|
|
88
|
+
},
|
|
89
|
+
"num_rows": {
|
|
90
|
+
"type": "integer",
|
|
91
|
+
"minimum": 0,
|
|
92
|
+
"description": "Total number of records."
|
|
93
|
+
},
|
|
94
|
+
"column_stats": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": { "$ref": "#/$defs/ColumnStats" },
|
|
97
|
+
"description": "Per-column statistics keyed by column name. The concrete shape depends on the column dtype; users must branch on the corresponding entry in `columns`."
|
|
98
|
+
},
|
|
99
|
+
"duplicate_row_fraction": {
|
|
100
|
+
"type": "number",
|
|
101
|
+
"minimum": 0,
|
|
102
|
+
"maximum": 1,
|
|
103
|
+
"description": "Estimated fraction of exact-duplicate rows."
|
|
104
|
+
},
|
|
105
|
+
"near_duplicate_estimate": {
|
|
106
|
+
"type": "number",
|
|
107
|
+
"minimum": 0,
|
|
108
|
+
"maximum": 1,
|
|
109
|
+
"description": "Approximate fraction of near-duplicate rows via MinHash/LSH."
|
|
110
|
+
},
|
|
111
|
+
"constant_columns": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"items": { "type": "string" },
|
|
114
|
+
"description": "Names of columns with zero variance / a single unique value."
|
|
115
|
+
},
|
|
116
|
+
"high_null_columns": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"items": { "type": "string" },
|
|
119
|
+
"description": "Names of columns where null_fraction > 0.5."
|
|
120
|
+
},
|
|
121
|
+
"row_minhash": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"contentEncoding": "base64",
|
|
124
|
+
"description": "Base64-encoded MinHash signature of the row set. Enables Jaccard similarity between snapshots."
|
|
125
|
+
},
|
|
126
|
+
"row_hyperloglog": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"contentEncoding": "base64",
|
|
129
|
+
"description": "Base64-encoded HyperLogLog sketch for cardinality estimation of unique rows."
|
|
130
|
+
},
|
|
131
|
+
"parent_snapshot_id": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"format": "uuid",
|
|
134
|
+
"description": "ID of the preceding snapshot of the same dataset, if one exists."
|
|
135
|
+
},
|
|
136
|
+
"pipeline_stage": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"enum": ["raw", "cleaned", "featurized", "augmented", "sampled", "final"],
|
|
139
|
+
"description": "Stage in the data pipeline at which this snapshot was taken."
|
|
140
|
+
},
|
|
141
|
+
"transform_description": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Human-readable description of the changes applied since the parent snapshot."
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"$defs": {
|
|
147
|
+
"ColumnDescriptor": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"required": ["name", "dtype", "nullable"],
|
|
150
|
+
"additionalProperties": false,
|
|
151
|
+
"properties": {
|
|
152
|
+
"name": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "Column identifier."
|
|
155
|
+
},
|
|
156
|
+
"dtype": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"examples": [
|
|
159
|
+
"int8", "int16", "int32", "int64",
|
|
160
|
+
"float16", "float32", "float64",
|
|
161
|
+
"string", "bool", "datetime64", "bytes"
|
|
162
|
+
],
|
|
163
|
+
"description": "Storage type of the column."
|
|
164
|
+
},
|
|
165
|
+
"nullable": {
|
|
166
|
+
"type": "boolean",
|
|
167
|
+
"description": "Whether null values are structurally allowed."
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"ColumnStats": {
|
|
172
|
+
"oneOf": [
|
|
173
|
+
{ "$ref": "#/$defs/NumericColumnStats" },
|
|
174
|
+
{ "$ref": "#/$defs/StringColumnStats" },
|
|
175
|
+
{ "$ref": "#/$defs/DatetimeColumnStats" }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"NumericColumnStats": {
|
|
179
|
+
"description": "Statistics for int* and float* columns.",
|
|
180
|
+
"type": "object",
|
|
181
|
+
"required": [
|
|
182
|
+
"null_count", "null_fraction",
|
|
183
|
+
"mean", "std", "min", "max",
|
|
184
|
+
"percentiles", "histogram", "num_unique"
|
|
185
|
+
],
|
|
186
|
+
"additionalProperties": false,
|
|
187
|
+
"properties": {
|
|
188
|
+
"null_count": { "type": "integer", "minimum": 0 },
|
|
189
|
+
"null_fraction": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
190
|
+
"mean": { "type": "number" },
|
|
191
|
+
"std": { "type": "number", "minimum": 0 },
|
|
192
|
+
"min": { "type": "number" },
|
|
193
|
+
"max": { "type": "number" },
|
|
194
|
+
"percentiles": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"required": ["p5", "p25", "p50", "p75", "p95", "p99"],
|
|
197
|
+
"additionalProperties": false,
|
|
198
|
+
"properties": {
|
|
199
|
+
"p5": { "type": "number" },
|
|
200
|
+
"p25": { "type": "number" },
|
|
201
|
+
"p50": { "type": "number" },
|
|
202
|
+
"p75": { "type": "number" },
|
|
203
|
+
"p95": { "type": "number" },
|
|
204
|
+
"p99": { "type": "number" }
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"histogram": {
|
|
208
|
+
"type": "object",
|
|
209
|
+
"required": ["bin_edges", "counts"],
|
|
210
|
+
"additionalProperties": false,
|
|
211
|
+
"properties": {
|
|
212
|
+
"bin_edges": {
|
|
213
|
+
"type": "array",
|
|
214
|
+
"items": { "type": "number" },
|
|
215
|
+
"description": "Length is num_bins + 1."
|
|
216
|
+
},
|
|
217
|
+
"counts": {
|
|
218
|
+
"type": "array",
|
|
219
|
+
"items": { "type": "integer", "minimum": 0 },
|
|
220
|
+
"description": "Length is num_bins."
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"num_unique": {
|
|
225
|
+
"type": "integer",
|
|
226
|
+
"minimum": 0,
|
|
227
|
+
"description": "Exact count if cheap to compute, otherwise a sketch estimate."
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"StringColumnStats": {
|
|
232
|
+
"description": "Statistics for string columns.",
|
|
233
|
+
"type": "object",
|
|
234
|
+
"required": [
|
|
235
|
+
"null_count", "null_fraction",
|
|
236
|
+
"num_unique", "top_values", "top_values_coverage",
|
|
237
|
+
"avg_char_length", "min_char_length", "max_char_length",
|
|
238
|
+
"avg_token_count", "min_token_count", "max_token_count"
|
|
239
|
+
],
|
|
240
|
+
"additionalProperties": false,
|
|
241
|
+
"properties": {
|
|
242
|
+
"null_count": { "type": "integer", "minimum": 0 },
|
|
243
|
+
"null_fraction": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
244
|
+
"num_unique": { "type": "integer", "minimum": 0, "description": "Cardinality." },
|
|
245
|
+
"top_values": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"additionalProperties": { "type": "integer", "minimum": 0 },
|
|
248
|
+
"description": "Top-K {value: count} pairs (default K=50)."
|
|
249
|
+
},
|
|
250
|
+
"top_values_coverage": {
|
|
251
|
+
"type": "number",
|
|
252
|
+
"minimum": 0,
|
|
253
|
+
"maximum": 1,
|
|
254
|
+
"description": "Fraction of rows covered by the top_values entries."
|
|
255
|
+
},
|
|
256
|
+
"avg_char_length": { "type": "number", "minimum": 0 },
|
|
257
|
+
"min_char_length": { "type": "integer", "minimum": 0 },
|
|
258
|
+
"max_char_length": { "type": "integer", "minimum": 0 },
|
|
259
|
+
"avg_token_count": {
|
|
260
|
+
"type": "number",
|
|
261
|
+
"minimum": 0,
|
|
262
|
+
"description": "Whitespace-split token count, averaged over non-null rows."
|
|
263
|
+
},
|
|
264
|
+
"min_token_count": { "type": "integer", "minimum": 0 },
|
|
265
|
+
"max_token_count": { "type": "integer", "minimum": 0 },
|
|
266
|
+
"detected_languages": {
|
|
267
|
+
"type": "object",
|
|
268
|
+
"additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
269
|
+
"description": "Top detected BCP-47 language codes mapped to their row fraction."
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"DatetimeColumnStats": {
|
|
274
|
+
"description": "Statistics for datetime64 columns.",
|
|
275
|
+
"type": "object",
|
|
276
|
+
"required": ["null_count", "null_fraction", "min", "max", "range_days"],
|
|
277
|
+
"additionalProperties": false,
|
|
278
|
+
"properties": {
|
|
279
|
+
"null_count": { "type": "integer", "minimum": 0 },
|
|
280
|
+
"null_fraction": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
281
|
+
"min": {
|
|
282
|
+
"type": "string",
|
|
283
|
+
"format": "date-time",
|
|
284
|
+
"description": "ISO-8601 lower boundary."
|
|
285
|
+
},
|
|
286
|
+
"max": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"format": "date-time",
|
|
289
|
+
"description": "ISO-8601 upper boundary."
|
|
290
|
+
},
|
|
291
|
+
"range_days": {
|
|
292
|
+
"type": "number",
|
|
293
|
+
"minimum": 0,
|
|
294
|
+
"description": "(max - min) expressed in days."
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|