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,46 @@
|
|
|
1
|
+
"""Snapshot computation for dstrack.
|
|
2
|
+
|
|
3
|
+
Classes
|
|
4
|
+
-------
|
|
5
|
+
[SnapshotBuilder][dstrack.snapshot._builder.SnapshotBuilder]
|
|
6
|
+
Builds a complete snapshot from a single reader, combining the two below.
|
|
7
|
+
|
|
8
|
+
[MetadataBuilder][dstrack.snapshot._metadata.MetadataBuilder]
|
|
9
|
+
Builds identity and structural snapshot fields from a reader's schema.
|
|
10
|
+
|
|
11
|
+
[StatsComputer][dstrack.snapshot._stats.StatsComputer]
|
|
12
|
+
Computes per-column and dataset-level statistics in a single data pass.
|
|
13
|
+
|
|
14
|
+
Result types
|
|
15
|
+
------------
|
|
16
|
+
[SnapshotMetadata][dstrack.snapshot._metadata.SnapshotMetadata],
|
|
17
|
+
[DatasetStats][dstrack.snapshot._stats.DatasetStats]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._builder import SnapshotBuilder, build_snapshot_dict
|
|
21
|
+
from ._metadata import MetadataBuilder, SnapshotMetadata
|
|
22
|
+
from ._stats import (
|
|
23
|
+
DatasetStats,
|
|
24
|
+
DatetimeColumnStats,
|
|
25
|
+
HistogramStats,
|
|
26
|
+
NumericColumnStats,
|
|
27
|
+
OtherColumnStats,
|
|
28
|
+
PercentileStats,
|
|
29
|
+
StatsComputer,
|
|
30
|
+
StringColumnStats,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DatasetStats",
|
|
35
|
+
"DatetimeColumnStats",
|
|
36
|
+
"HistogramStats",
|
|
37
|
+
"MetadataBuilder",
|
|
38
|
+
"NumericColumnStats",
|
|
39
|
+
"OtherColumnStats",
|
|
40
|
+
"PercentileStats",
|
|
41
|
+
"SnapshotBuilder",
|
|
42
|
+
"SnapshotMetadata",
|
|
43
|
+
"StatsComputer",
|
|
44
|
+
"StringColumnStats",
|
|
45
|
+
"build_snapshot_dict",
|
|
46
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from pathlib import Path, PurePath
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from dstrack.readers import TabularReader
|
|
6
|
+
from dstrack.snapshot._metadata import MetadataBuilder, SnapshotMetadata
|
|
7
|
+
from dstrack.snapshot._stats import DatasetStats, StatsComputer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SnapshotBuilder:
|
|
11
|
+
"""Builds a complete dataset snapshot from a single reader.
|
|
12
|
+
|
|
13
|
+
Combines [MetadataBuilder][dstrack.snapshot._metadata.MetadataBuilder]
|
|
14
|
+
(identity and schema) and
|
|
15
|
+
[StatsComputer][dstrack.snapshot._stats.StatsComputer] (a data pass over
|
|
16
|
+
the rows) into one JSON-ready snapshot dict. Import it to build snapshots
|
|
17
|
+
from Python without going through the CLI:
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> from dstrack.readers import CsvReader
|
|
21
|
+
>>> from dstrack.snapshot import SnapshotBuilder
|
|
22
|
+
>>> reader = CsvReader("data.csv") # doctest: +SKIP
|
|
23
|
+
>>> snapshot = SnapshotBuilder().build( # doctest: +SKIP
|
|
24
|
+
... reader,
|
|
25
|
+
... dataset_name="customers",
|
|
26
|
+
... dataset_path="data.csv",
|
|
27
|
+
... created_by="alice",
|
|
28
|
+
... )
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
metadata_builder: Builder used for identity and schema fields.
|
|
32
|
+
Defaults to a fresh
|
|
33
|
+
[MetadataBuilder][dstrack.snapshot._metadata.MetadataBuilder].
|
|
34
|
+
stats_computer: Computer used for the per-column and dataset-level
|
|
35
|
+
statistics. Defaults to a fresh
|
|
36
|
+
[StatsComputer][dstrack.snapshot._stats.StatsComputer]. Pass a
|
|
37
|
+
pre-configured ``StatsComputer(max_rows=...)`` here to change the
|
|
38
|
+
in-memory row limit the statistics pass enforces.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
metadata_builder: MetadataBuilder | None = None,
|
|
45
|
+
stats_computer: StatsComputer | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
self._metadata_builder = metadata_builder or MetadataBuilder()
|
|
48
|
+
self._stats_computer = stats_computer or StatsComputer()
|
|
49
|
+
|
|
50
|
+
def build(
|
|
51
|
+
self,
|
|
52
|
+
reader: TabularReader,
|
|
53
|
+
*,
|
|
54
|
+
dataset_name: str,
|
|
55
|
+
dataset_path: str | PurePath,
|
|
56
|
+
created_by: str,
|
|
57
|
+
source_type: str = "file",
|
|
58
|
+
source: str | Path | None = None,
|
|
59
|
+
source_hash: str | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Build a JSON-serializable snapshot from a reader.
|
|
62
|
+
|
|
63
|
+
The reader is consumed once for schema inference and once for the
|
|
64
|
+
statistics data pass.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
reader: Any [TabularReader][dstrack.readers._protocol.TabularReader].
|
|
68
|
+
dataset_name: Human-readable dataset name stored in the snapshot.
|
|
69
|
+
dataset_path: Source path or URI recorded in the snapshot as a
|
|
70
|
+
forward-slash string. Never opened; pass ``source`` when the
|
|
71
|
+
data lives elsewhere.
|
|
72
|
+
created_by: User or process identifier.
|
|
73
|
+
source_type: Origin kind (``"file"``, ``"directory"``, etc.).
|
|
74
|
+
source: Location the data actually lives at, used only to compute
|
|
75
|
+
``source_hash``. Defaults to ``dataset_path``.
|
|
76
|
+
source_hash: Pre-computed source hash. When ``None`` and the
|
|
77
|
+
source resolves to a regular file, a SHA-256 of the file bytes
|
|
78
|
+
is computed automatically by the metadata builder.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A ``dict`` merging every metadata and statistics field, ready to be
|
|
82
|
+
serialized with [json.dumps][json.dumps].
|
|
83
|
+
"""
|
|
84
|
+
metadata = self._metadata_builder.build(
|
|
85
|
+
reader,
|
|
86
|
+
dataset_name=dataset_name,
|
|
87
|
+
dataset_path=dataset_path,
|
|
88
|
+
source_type=source_type,
|
|
89
|
+
created_by=created_by,
|
|
90
|
+
source=source,
|
|
91
|
+
source_hash=source_hash,
|
|
92
|
+
)
|
|
93
|
+
stats = self._stats_computer.compute(reader)
|
|
94
|
+
return build_snapshot_dict(metadata, stats)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_snapshot_dict(
|
|
98
|
+
metadata: SnapshotMetadata, stats: DatasetStats
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Merge metadata and statistics into one JSON-serializable snapshot dict.
|
|
101
|
+
|
|
102
|
+
The two inputs contribute disjoint sets of keys, so the merge never drops
|
|
103
|
+
a field.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
metadata: Identity and schema fields from
|
|
107
|
+
[MetadataBuilder][dstrack.snapshot._metadata.MetadataBuilder].
|
|
108
|
+
stats: Volume, per-column, and quality fields from
|
|
109
|
+
[StatsComputer][dstrack.snapshot._stats.StatsComputer].
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A ``dict`` combining both, with nested dataclasses expanded to dicts.
|
|
113
|
+
"""
|
|
114
|
+
return {**dataclasses.asdict(metadata), **dataclasses.asdict(stats)}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import uuid
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path, PurePath
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dstrack.readers import ColumnInfo, TabularReader
|
|
9
|
+
from dstrack.snapshot._version import FORMAT_VERSION
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SnapshotMetadata:
|
|
14
|
+
"""Identity and structural fields for a snapshot.
|
|
15
|
+
|
|
16
|
+
Covers every required top-level field that does not require a data pass:
|
|
17
|
+
versioning identifiers, authorship, schema shape, and the source hash.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
format_version: str
|
|
21
|
+
snapshot_id: str
|
|
22
|
+
created_at: str
|
|
23
|
+
created_by: str
|
|
24
|
+
dataset_name: str
|
|
25
|
+
dataset_path: str
|
|
26
|
+
source_type: str
|
|
27
|
+
source_hash: str
|
|
28
|
+
num_columns: int
|
|
29
|
+
columns: list[dict[str, Any]]
|
|
30
|
+
schema_hash: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MetadataBuilder:
|
|
34
|
+
"""Builds identity and structural metadata without reading data rows.
|
|
35
|
+
|
|
36
|
+
Computes snapshot_id, created_at, created_by, dataset_name,
|
|
37
|
+
dataset_path, source_type, source_hash, num_columns, columns, and
|
|
38
|
+
schema_hash from the reader's column descriptors alone.
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
schema_hash is order-independent: reordering a dataset's columns
|
|
42
|
+
without changing their names or dtypes produces the same hash.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def build(
|
|
46
|
+
self,
|
|
47
|
+
reader: TabularReader,
|
|
48
|
+
*,
|
|
49
|
+
dataset_name: str,
|
|
50
|
+
dataset_path: str | PurePath,
|
|
51
|
+
source_type: str,
|
|
52
|
+
created_by: str,
|
|
53
|
+
source: str | Path | None = None,
|
|
54
|
+
source_hash: str | None = None,
|
|
55
|
+
) -> SnapshotMetadata:
|
|
56
|
+
"""Build metadata for a snapshot.
|
|
57
|
+
|
|
58
|
+
``dataset_path`` is what gets *recorded*; ``source`` is what gets
|
|
59
|
+
*read*. They differ whenever the recorded path is relative to a path
|
|
60
|
+
root that is not the current working directory, as it is for snapshots
|
|
61
|
+
written by the CLI.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
reader: Any [TabularReader][dstrack.readers._protocol.TabularReader];
|
|
65
|
+
only [columns()][dstrack.readers._protocol.TabularReader.columns]
|
|
66
|
+
is called.
|
|
67
|
+
dataset_name: Human-readable dataset name stored in the snapshot.
|
|
68
|
+
dataset_path: Source path or URI at snapshot time, recorded in the
|
|
69
|
+
snapshot as a forward-slash string. Never opened.
|
|
70
|
+
source_type: Origin kind (``"file"``, ``"directory"``, etc.).
|
|
71
|
+
created_by: User or process identifier.
|
|
72
|
+
source: Location the data actually lives at, used only to compute
|
|
73
|
+
``source_hash``. Defaults to ``dataset_path``, which is
|
|
74
|
+
correct when the recorded path is one the process can open.
|
|
75
|
+
source_hash: Pre-computed source hash. When ``None`` and the
|
|
76
|
+
source resolves to a regular file, a SHA-256 of the file bytes
|
|
77
|
+
is computed automatically.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A populated
|
|
81
|
+
[SnapshotMetadata][dstrack.snapshot._metadata.SnapshotMetadata]
|
|
82
|
+
instance.
|
|
83
|
+
"""
|
|
84
|
+
cols = reader.columns()
|
|
85
|
+
# Recorded with forward slashes so a store written on Windows still
|
|
86
|
+
# matches the same dataset when read on POSIX, and vice versa.
|
|
87
|
+
path_str = (
|
|
88
|
+
dataset_path.as_posix()
|
|
89
|
+
if isinstance(dataset_path, PurePath)
|
|
90
|
+
else str(dataset_path)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if source_hash is None:
|
|
94
|
+
p = Path(source if source is not None else dataset_path)
|
|
95
|
+
source_hash = _hash_file(p) if p.is_file() else ""
|
|
96
|
+
|
|
97
|
+
return SnapshotMetadata(
|
|
98
|
+
format_version=FORMAT_VERSION,
|
|
99
|
+
snapshot_id=str(uuid.uuid4()),
|
|
100
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
101
|
+
created_by=created_by,
|
|
102
|
+
dataset_name=dataset_name,
|
|
103
|
+
dataset_path=path_str,
|
|
104
|
+
source_type=source_type,
|
|
105
|
+
source_hash=source_hash,
|
|
106
|
+
num_columns=len(cols),
|
|
107
|
+
columns=_columns_to_dicts(cols),
|
|
108
|
+
schema_hash=_schema_hash(cols),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _hash_file(path: Path) -> str:
|
|
113
|
+
"""Compute the SHA-256 hex digest of a file's contents.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
path: Path to the file to hash.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The hex-encoded SHA-256 digest of the file's bytes.
|
|
120
|
+
"""
|
|
121
|
+
h = hashlib.sha256()
|
|
122
|
+
with path.open("rb") as fh:
|
|
123
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
124
|
+
h.update(chunk)
|
|
125
|
+
return h.hexdigest()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _columns_to_dicts(cols: list[ColumnInfo]) -> list[dict[str, Any]]:
|
|
129
|
+
"""Convert column descriptors into plain dicts for serialization.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
cols: Column descriptors as returned by a reader.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
One dict per column with ``name``, ``dtype``, and ``nullable`` keys.
|
|
136
|
+
"""
|
|
137
|
+
return [{"name": c.name, "dtype": c.dtype, "nullable": c.nullable} for c in cols]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _schema_hash(cols: list[ColumnInfo]) -> str:
|
|
141
|
+
"""Compute a deterministic hash of a schema's column names and dtypes.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
cols: Column descriptors, in any order.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A hex-encoded SHA-256 digest that changes if any column's name or
|
|
148
|
+
dtype changes. Order-independent: reordering ``cols`` does not
|
|
149
|
+
change the result. Ignores ``nullable``.
|
|
150
|
+
"""
|
|
151
|
+
h = hashlib.sha256()
|
|
152
|
+
for name, dtype in sorted((c.name, c.dtype) for c in cols):
|
|
153
|
+
h.update(f"{name}\x00{dtype}\n".encode())
|
|
154
|
+
return h.hexdigest()
|