dstrack 0.1.0__tar.gz → 0.2.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.
- {dstrack-0.1.0 → dstrack-0.2.0}/PKG-INFO +35 -5
- {dstrack-0.1.0 → dstrack-0.2.0}/README.md +32 -4
- dstrack-0.2.0/pyproject.toml +124 -0
- dstrack-0.1.0/pyproject.toml → dstrack-0.2.0/pyproject.toml.orig +15 -2
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_cli.py +8 -0
- dstrack-0.2.0/src/dstrack/_diff.py +235 -0
- dstrack-0.2.0/src/dstrack/_diff_render.py +925 -0
- dstrack-0.2.0/src/dstrack/_diff_report.py +1020 -0
- dstrack-0.2.0/src/dstrack/_log.py +125 -0
- dstrack-0.2.0/src/dstrack/_resolve.py +602 -0
- dstrack-0.2.0/src/dstrack/assets/diff-report.png +0 -0
- dstrack-0.2.0/src/dstrack/assets/dstrack-logo.png +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/cache.py +81 -4
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/console.py +10 -0
- dstrack-0.2.0/src/dstrack/diff/__init__.py +103 -0
- dstrack-0.2.0/src/dstrack/diff/_engine.py +1252 -0
- dstrack-0.2.0/src/dstrack/diff/_extract.py +305 -0
- dstrack-0.2.0/src/dstrack/diff/_model.py +511 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/errors.py +16 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/schemas/snapshot_v1.json +1 -1
- dstrack-0.2.0/src/dstrack/snapshot/_version.py +25 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/store.py +92 -3
- dstrack-0.1.0/src/dstrack/_log.py +0 -268
- dstrack-0.1.0/src/dstrack/snapshot/_version.py +0 -13
- {dstrack-0.1.0 → dstrack-0.2.0}/LICENSE +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/__init__.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/__init__.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_cli.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_environment.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_profiling.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_report.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_runner.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_synthetic.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_log_render.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_logging.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_store_template.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_track.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/paths.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/__init__.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_csv.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_protocol.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_registry.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_resolve.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/__init__.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_builder.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_metadata.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_stats.py +0 -0
- {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dstrack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Python package for versioning and monitoring dataset changes throughout the machine learning lifecycle.
|
|
5
5
|
Keywords: dataset,versioning,monitoring,machine learning,mlops,data science
|
|
6
6
|
Author: Leonardo Ayala
|
|
@@ -19,8 +19,10 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Requires-Dist: rich>=15.0.0
|
|
21
21
|
Requires-Dist: typer>=0.26.8
|
|
22
|
+
Requires-Dist: reportlab>=4.2 ; extra == 'report'
|
|
22
23
|
Requires-Python: >=3.11
|
|
23
24
|
Project-URL: GitHub, https://github.com/leoyala/dstrack
|
|
25
|
+
Provides-Extra: report
|
|
24
26
|
Description-Content-Type: text/markdown
|
|
25
27
|
|
|
26
28
|
# dstrack
|
|
@@ -29,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
29
31
|
[](https://codecov.io/github/leoyala/dstrack)
|
|
30
32
|
|
|
31
33
|
<p align="center">
|
|
32
|
-
<img src="
|
|
34
|
+
<img src="src/dstrack/assets/dstrack-logo.png" width="200" alt="dstrack logo">
|
|
33
35
|
</p>
|
|
34
36
|
|
|
35
37
|
|
|
@@ -39,6 +41,12 @@ A Python package for versioning and monitoring dataset changes throughout the ma
|
|
|
39
41
|
<img src="docs/assets/cli.gif" alt="CLI example">
|
|
40
42
|
</p>
|
|
41
43
|
|
|
44
|
+
|
|
45
|
+
<figure markdown="span">
|
|
46
|
+
<img src="src/dstrack/assets/diff-report.png" alt="PDF Diff report">
|
|
47
|
+
<figcaption>Exemplary PDF report (1st page only)</figcaption>
|
|
48
|
+
</figure>
|
|
49
|
+
|
|
42
50
|
## Overview
|
|
43
51
|
|
|
44
52
|
`dstrack` helps data scientists and ML engineers track how datasets evolve over time, catching schema drift, distribution shifts, and unexpected mutations before they silently break pipelines or degrade model performance.
|
|
@@ -109,15 +117,37 @@ Each snapshot captures the file's schema, a content fingerprint, and per-column
|
|
|
109
117
|
statistics. See the [Getting Started guide](https://leoyala.github.io/dstrack/getting_started/)
|
|
110
118
|
for options such as `--name`, `--reader`, and `--dataset-id`.
|
|
111
119
|
|
|
120
|
+
**4. See what changed** - `dstrack log` shows *that* a dataset changed;
|
|
121
|
+
`dstrack diff` shows *what* changed:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dstrack diff data.csv
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The two snapshots are shown side by side, with the schema changes, the dataset's own
|
|
128
|
+
numbers, and every column whose statistics moved, each colored by how much it should
|
|
129
|
+
worry you. Add `--json` for a pipeline to read, or `--pdf report.pdf` for a formatted
|
|
130
|
+
report to share (needs the `report` extra).
|
|
131
|
+
|
|
132
|
+
It is a library too, so a comparison can be done entirely from Python:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from dstrack.diff import diff_snapshots
|
|
136
|
+
|
|
137
|
+
result = diff_snapshots(old_payload, new_payload)
|
|
138
|
+
print(result.severity, result.summary)
|
|
139
|
+
```
|
|
140
|
+
|
|
112
141
|
## Features
|
|
113
142
|
|
|
114
143
|
- **Dataset versioning** - snapshot a dataset and track its lineage across pipeline stages
|
|
115
144
|
- **Rich snapshots** - schema hash, content fingerprint, and per-column statistics (ranges, null rates, and more)
|
|
145
|
+
- **Snapshot comparison** - schema, statistics and distribution-shift diffs, in the terminal, as JSON, or as a PDF report
|
|
116
146
|
- **CSV out of the box** - pure standard-library reader, no heavy dependencies
|
|
117
147
|
- **Lightweight CLI** - a small, git-like local store you can commit alongside your code
|
|
118
148
|
|
|
119
149
|
## Roadmap
|
|
120
150
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
Drift monitoring in CI is next - comparing a working file against its latest snapshot
|
|
152
|
+
and failing the build when data drifts. See the [roadmap](docs/roadmap.md) for what's
|
|
153
|
+
planned.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://codecov.io/github/leoyala/dstrack)
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="
|
|
7
|
+
<img src="src/dstrack/assets/dstrack-logo.png" width="200" alt="dstrack logo">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
|
|
@@ -14,6 +14,12 @@ A Python package for versioning and monitoring dataset changes throughout the ma
|
|
|
14
14
|
<img src="docs/assets/cli.gif" alt="CLI example">
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
+
|
|
18
|
+
<figure markdown="span">
|
|
19
|
+
<img src="src/dstrack/assets/diff-report.png" alt="PDF Diff report">
|
|
20
|
+
<figcaption>Exemplary PDF report (1st page only)</figcaption>
|
|
21
|
+
</figure>
|
|
22
|
+
|
|
17
23
|
## Overview
|
|
18
24
|
|
|
19
25
|
`dstrack` helps data scientists and ML engineers track how datasets evolve over time, catching schema drift, distribution shifts, and unexpected mutations before they silently break pipelines or degrade model performance.
|
|
@@ -84,15 +90,37 @@ Each snapshot captures the file's schema, a content fingerprint, and per-column
|
|
|
84
90
|
statistics. See the [Getting Started guide](https://leoyala.github.io/dstrack/getting_started/)
|
|
85
91
|
for options such as `--name`, `--reader`, and `--dataset-id`.
|
|
86
92
|
|
|
93
|
+
**4. See what changed** - `dstrack log` shows *that* a dataset changed;
|
|
94
|
+
`dstrack diff` shows *what* changed:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
dstrack diff data.csv
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The two snapshots are shown side by side, with the schema changes, the dataset's own
|
|
101
|
+
numbers, and every column whose statistics moved, each colored by how much it should
|
|
102
|
+
worry you. Add `--json` for a pipeline to read, or `--pdf report.pdf` for a formatted
|
|
103
|
+
report to share (needs the `report` extra).
|
|
104
|
+
|
|
105
|
+
It is a library too, so a comparison can be done entirely from Python:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from dstrack.diff import diff_snapshots
|
|
109
|
+
|
|
110
|
+
result = diff_snapshots(old_payload, new_payload)
|
|
111
|
+
print(result.severity, result.summary)
|
|
112
|
+
```
|
|
113
|
+
|
|
87
114
|
## Features
|
|
88
115
|
|
|
89
116
|
- **Dataset versioning** - snapshot a dataset and track its lineage across pipeline stages
|
|
90
117
|
- **Rich snapshots** - schema hash, content fingerprint, and per-column statistics (ranges, null rates, and more)
|
|
118
|
+
- **Snapshot comparison** - schema, statistics and distribution-shift diffs, in the terminal, as JSON, or as a PDF report
|
|
91
119
|
- **CSV out of the box** - pure standard-library reader, no heavy dependencies
|
|
92
120
|
- **Lightweight CLI** - a small, git-like local store you can commit alongside your code
|
|
93
121
|
|
|
94
122
|
## Roadmap
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
Drift monitoring in CI is next - comparing a working file against its latest snapshot
|
|
125
|
+
and failing the build when data drifts. See the [roadmap](docs/roadmap.md) for what's
|
|
126
|
+
planned.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dstrack"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "A Python package for versioning and monitoring dataset changes throughout the machine learning lifecycle."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"dataset",
|
|
10
|
+
"versioning",
|
|
11
|
+
"monitoring",
|
|
12
|
+
"machine learning",
|
|
13
|
+
"mlops",
|
|
14
|
+
"data science",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.11"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"rich>=15.0.0",
|
|
32
|
+
"typer>=0.26.8",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[[project.authors]]
|
|
36
|
+
name = "Leonardo Ayala"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
report = ["reportlab>=4.2"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
GitHub = "https://github.com/leoyala/dstrack"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
dstrack = "dstrack._cli:app"
|
|
46
|
+
dstrack-benchmark = "dstrack._benchmark:app"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.11.28,<0.13.0"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 88
|
|
54
|
+
src = [
|
|
55
|
+
"src",
|
|
56
|
+
"tests",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E",
|
|
62
|
+
"W",
|
|
63
|
+
"F",
|
|
64
|
+
"I",
|
|
65
|
+
"B",
|
|
66
|
+
"UP",
|
|
67
|
+
"SIM",
|
|
68
|
+
"C4",
|
|
69
|
+
"PERF",
|
|
70
|
+
"RUF",
|
|
71
|
+
]
|
|
72
|
+
ignore = ["E501"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"tests/**" = [
|
|
76
|
+
"B",
|
|
77
|
+
"S101",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.pydocstyle]
|
|
81
|
+
convention = "google"
|
|
82
|
+
|
|
83
|
+
[tool.ruff.format]
|
|
84
|
+
docstring-code-format = true
|
|
85
|
+
quote-style = "double"
|
|
86
|
+
|
|
87
|
+
[tool.mypy]
|
|
88
|
+
files = ["src"]
|
|
89
|
+
python_version = "3.11"
|
|
90
|
+
strict = true
|
|
91
|
+
show_error_codes = true
|
|
92
|
+
warn_unreachable = true
|
|
93
|
+
warn_redundant_casts = true
|
|
94
|
+
warn_unused_ignores = true
|
|
95
|
+
enable_error_code = [
|
|
96
|
+
"ignore-without-code",
|
|
97
|
+
"redundant-expr",
|
|
98
|
+
"truthy-bool",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[[tool.mypy.overrides]]
|
|
102
|
+
module = [
|
|
103
|
+
"numpy.*",
|
|
104
|
+
"pandas.*",
|
|
105
|
+
"reportlab.*",
|
|
106
|
+
"sklearn.*",
|
|
107
|
+
]
|
|
108
|
+
ignore_missing_imports = true
|
|
109
|
+
|
|
110
|
+
[tool.coverage.run]
|
|
111
|
+
omit = ["sample_data.py"]
|
|
112
|
+
|
|
113
|
+
[tool.codespell]
|
|
114
|
+
ignore-words-list = "nd,hist,ans"
|
|
115
|
+
skip = "*.lock,*.csv,*.parquet,./data,./datasets"
|
|
116
|
+
|
|
117
|
+
[dependency-groups]
|
|
118
|
+
dev = [
|
|
119
|
+
"jsonschema>=4.26.0",
|
|
120
|
+
"mkdocstrings-python>=2.0.5",
|
|
121
|
+
"pytest>=9.1.1",
|
|
122
|
+
"pytest-cov>=7.1.0",
|
|
123
|
+
"zensical>=0.0.50",
|
|
124
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dstrack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A Python package for versioning and monitoring dataset changes throughout the machine learning lifecycle."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -28,6 +28,11 @@ dependencies = [
|
|
|
28
28
|
"typer>=0.26.8",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# PDF reporting only. Kept out of the base install so the core stays as
|
|
33
|
+
# dependency-light as it is: most runs never write a report.
|
|
34
|
+
report = ["reportlab>=4.2"]
|
|
35
|
+
|
|
31
36
|
[project.urls]
|
|
32
37
|
GitHub = "https://github.com/leoyala/dstrack"
|
|
33
38
|
|
|
@@ -36,7 +41,7 @@ dstrack = "dstrack._cli:app"
|
|
|
36
41
|
dstrack-benchmark = "dstrack._benchmark:app"
|
|
37
42
|
|
|
38
43
|
[build-system]
|
|
39
|
-
requires = ["uv_build>=0.11.28,<0.
|
|
44
|
+
requires = ["uv_build>=0.11.28,<0.13.0"]
|
|
40
45
|
build-backend = "uv_build"
|
|
41
46
|
|
|
42
47
|
# -----------------------------------------------------------------------------
|
|
@@ -93,10 +98,18 @@ enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
|
|
|
93
98
|
module = [
|
|
94
99
|
"numpy.*",
|
|
95
100
|
"pandas.*",
|
|
101
|
+
"reportlab.*",
|
|
96
102
|
"sklearn.*",
|
|
97
103
|
]
|
|
98
104
|
ignore_missing_imports = true
|
|
99
105
|
|
|
106
|
+
# -----------------------------------------------------------------------------
|
|
107
|
+
# coverage
|
|
108
|
+
# -----------------------------------------------------------------------------
|
|
109
|
+
[tool.coverage.run]
|
|
110
|
+
# Manual helper for trying out `dstrack diff`; deliberately untested.
|
|
111
|
+
omit = ["sample_data.py"]
|
|
112
|
+
|
|
100
113
|
# -----------------------------------------------------------------------------
|
|
101
114
|
# codespell: spell-checking for code, docstrings, and docs
|
|
102
115
|
# -----------------------------------------------------------------------------
|
|
@@ -6,6 +6,7 @@ from typing import Annotated
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
8
|
from dstrack import __version__, console
|
|
9
|
+
from dstrack._diff import diff
|
|
9
10
|
from dstrack._log import log
|
|
10
11
|
from dstrack._store_template import STORE_TEMPLATE, materialize
|
|
11
12
|
from dstrack._track import track
|
|
@@ -95,6 +96,13 @@ app.command(
|
|
|
95
96
|
"or its path."
|
|
96
97
|
)(log)
|
|
97
98
|
|
|
99
|
+
app.command(
|
|
100
|
+
help="Compare two snapshots and show what changed between them, side by "
|
|
101
|
+
"side. Each argument may be a snapshot id, a dataset id, or the path to a "
|
|
102
|
+
"tracked dataset; given one, it is compared against the snapshot it "
|
|
103
|
+
"succeeded."
|
|
104
|
+
)(diff)
|
|
105
|
+
|
|
98
106
|
|
|
99
107
|
@app.command(help="Print package version.")
|
|
100
108
|
def version() -> None:
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""The ``dstrack diff`` command: compare two snapshots of a dataset."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from dstrack import cache, console
|
|
12
|
+
from dstrack._diff_render import render_diff
|
|
13
|
+
from dstrack._resolve import ensure_index, resolve_pair
|
|
14
|
+
from dstrack.diff import PRESETS, DiffOptions, SnapshotDiff, diff_snapshots, preset
|
|
15
|
+
from dstrack.errors import (
|
|
16
|
+
AmbiguousSnapshotError,
|
|
17
|
+
DatasetNotFoundError,
|
|
18
|
+
IncompatibleSnapshotError,
|
|
19
|
+
MissingDependencyError,
|
|
20
|
+
SnapshotNotFoundError,
|
|
21
|
+
StoreCorruptionError,
|
|
22
|
+
StoreNotFoundError,
|
|
23
|
+
)
|
|
24
|
+
from dstrack.paths import resolve_store_root
|
|
25
|
+
|
|
26
|
+
# Failures that mean "what you asked for is not in the store, or is not
|
|
27
|
+
# readable", all of which are reported the same way: a message, and exit 1.
|
|
28
|
+
_USER_ERRORS = (
|
|
29
|
+
AmbiguousSnapshotError,
|
|
30
|
+
DatasetNotFoundError,
|
|
31
|
+
IncompatibleSnapshotError,
|
|
32
|
+
SnapshotNotFoundError,
|
|
33
|
+
StoreCorruptionError,
|
|
34
|
+
ValueError,
|
|
35
|
+
OSError,
|
|
36
|
+
sqlite3.DatabaseError,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def diff(
|
|
41
|
+
refs: Annotated[
|
|
42
|
+
list[str] | None,
|
|
43
|
+
typer.Argument(
|
|
44
|
+
help="Snapshots to compare: a snapshot id (in full or by its "
|
|
45
|
+
"leading characters), a dataset id, or the path to a tracked "
|
|
46
|
+
"dataset file. Given one, it is compared against the snapshot it "
|
|
47
|
+
"succeeded; given two, the first is the baseline.",
|
|
48
|
+
),
|
|
49
|
+
] = None,
|
|
50
|
+
from_ref: Annotated[
|
|
51
|
+
str | None,
|
|
52
|
+
typer.Option(
|
|
53
|
+
"--from",
|
|
54
|
+
help="Baseline to compare against. Alone, the dataset's latest "
|
|
55
|
+
"snapshot is compared to it.",
|
|
56
|
+
),
|
|
57
|
+
] = None,
|
|
58
|
+
to_ref: Annotated[
|
|
59
|
+
str | None,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--to",
|
|
62
|
+
help="Snapshot to compare. Alone, it is compared against the one "
|
|
63
|
+
"it succeeded.",
|
|
64
|
+
),
|
|
65
|
+
] = None,
|
|
66
|
+
sensitivity: Annotated[
|
|
67
|
+
str,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--sensitivity",
|
|
70
|
+
help="How much movement counts as significant: low, normal, or high.",
|
|
71
|
+
),
|
|
72
|
+
] = "normal",
|
|
73
|
+
show_all: Annotated[
|
|
74
|
+
bool,
|
|
75
|
+
typer.Option("--all", help="Include columns and metrics that did not change."),
|
|
76
|
+
] = False,
|
|
77
|
+
oneline: Annotated[
|
|
78
|
+
bool,
|
|
79
|
+
typer.Option("--oneline", help="Condense the comparison to a single line."),
|
|
80
|
+
] = False,
|
|
81
|
+
as_json: Annotated[
|
|
82
|
+
bool,
|
|
83
|
+
typer.Option(
|
|
84
|
+
"--json",
|
|
85
|
+
help="Print the comparison as JSON instead of a table. Implies a "
|
|
86
|
+
"non-interactive run, so pass `--yes` in scripts.",
|
|
87
|
+
),
|
|
88
|
+
] = False,
|
|
89
|
+
pdf: Annotated[
|
|
90
|
+
Path | None,
|
|
91
|
+
typer.Option(
|
|
92
|
+
"--pdf",
|
|
93
|
+
dir_okay=False,
|
|
94
|
+
writable=True,
|
|
95
|
+
help="Also write a PDF report of the comparison to this path. "
|
|
96
|
+
"Needs the `report` extra: pip install 'dstrack[report]'.",
|
|
97
|
+
),
|
|
98
|
+
] = None,
|
|
99
|
+
yes: Annotated[
|
|
100
|
+
bool,
|
|
101
|
+
typer.Option(
|
|
102
|
+
"-y",
|
|
103
|
+
"--yes",
|
|
104
|
+
help="Build the snapshot index without asking, if it does not exist yet.",
|
|
105
|
+
),
|
|
106
|
+
] = False,
|
|
107
|
+
rebuild: Annotated[
|
|
108
|
+
bool,
|
|
109
|
+
typer.Option(
|
|
110
|
+
"--rebuild",
|
|
111
|
+
help="Discard the snapshot index and rebuild it from the dataset logs.",
|
|
112
|
+
),
|
|
113
|
+
] = False,
|
|
114
|
+
root: Annotated[
|
|
115
|
+
Path | None,
|
|
116
|
+
typer.Option(
|
|
117
|
+
"--root",
|
|
118
|
+
help="Path root a REF path is made relative to, matching the one "
|
|
119
|
+
"`dstrack track` recorded it with. Defaults to the store root (the "
|
|
120
|
+
"directory containing .dstrack/). Ignored for a REF that is an id.",
|
|
121
|
+
),
|
|
122
|
+
] = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Compare two snapshots and report what changed between them."""
|
|
125
|
+
options = _options(sensitivity)
|
|
126
|
+
|
|
127
|
+
# Locate the .dstrack/ store; without one there is nothing to compare.
|
|
128
|
+
try:
|
|
129
|
+
store_root = resolve_store_root()
|
|
130
|
+
except StoreNotFoundError as e:
|
|
131
|
+
console.error(str(e))
|
|
132
|
+
raise typer.Exit(code=1) from e
|
|
133
|
+
|
|
134
|
+
# Under --json the only thing on stdout may be the document itself, so the
|
|
135
|
+
# index cannot be offered interactively: without one, --yes or --rebuild
|
|
136
|
+
# must be given, or the request is rejected.
|
|
137
|
+
ensure_index(store_root, yes=yes, rebuild=rebuild, quiet=as_json)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Sync before resolving: the index does not travel with the repository,
|
|
141
|
+
# so a snapshot taken elsewhere is only visible once its log is read.
|
|
142
|
+
reason = cache.sync(store_root)
|
|
143
|
+
if reason is not None and not as_json:
|
|
144
|
+
console.warning(f"Rebuilt the snapshot index: {reason}.")
|
|
145
|
+
|
|
146
|
+
old, new = resolve_pair(
|
|
147
|
+
refs or [],
|
|
148
|
+
from_ref=from_ref,
|
|
149
|
+
to_ref=to_ref,
|
|
150
|
+
store_root=store_root,
|
|
151
|
+
root=root,
|
|
152
|
+
)
|
|
153
|
+
result = diff_snapshots(
|
|
154
|
+
old.payload,
|
|
155
|
+
new.payload,
|
|
156
|
+
options=options,
|
|
157
|
+
old_dataset_id=old.dataset_id,
|
|
158
|
+
new_dataset_id=new.dataset_id,
|
|
159
|
+
)
|
|
160
|
+
except _USER_ERRORS as e:
|
|
161
|
+
console.error(str(e))
|
|
162
|
+
raise typer.Exit(code=1) from e
|
|
163
|
+
|
|
164
|
+
if pdf is not None and not _write_pdf(result, pdf, quiet=as_json):
|
|
165
|
+
raise typer.Exit(code=1)
|
|
166
|
+
|
|
167
|
+
if as_json:
|
|
168
|
+
# Deliberately not `console.display`: the document is data, and rich
|
|
169
|
+
# would wrap and highlight it into something no parser accepts.
|
|
170
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if not result.same_dataset:
|
|
174
|
+
console.warning("Comparing snapshots from two different datasets.")
|
|
175
|
+
|
|
176
|
+
console.display(
|
|
177
|
+
render_diff(
|
|
178
|
+
result,
|
|
179
|
+
width=console.width(),
|
|
180
|
+
now=datetime.now(UTC),
|
|
181
|
+
oneline=oneline,
|
|
182
|
+
show_unchanged=show_all,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _options(sensitivity: str) -> DiffOptions:
|
|
188
|
+
"""Return the thresholds a named sensitivity stands for.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
sensitivity: The preset name, as the user gave it.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Its thresholds.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
typer.BadParameter: If no preset goes by that name. Raised rather than
|
|
198
|
+
reported, so the failure reads like any other bad option.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
return preset(sensitivity)
|
|
202
|
+
except KeyError as e:
|
|
203
|
+
known = ", ".join(sorted(PRESETS))
|
|
204
|
+
raise typer.BadParameter(
|
|
205
|
+
f"Unknown sensitivity {sensitivity!r}. Choose one of: {known}."
|
|
206
|
+
) from e
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _write_pdf(result: SnapshotDiff, path: Path, *, quiet: bool) -> bool:
|
|
210
|
+
"""Write the PDF report, reporting a failure rather than raising it.
|
|
211
|
+
|
|
212
|
+
The reporting module is imported here rather than at the top of the file
|
|
213
|
+
because it needs an optional dependency: importing it eagerly would make
|
|
214
|
+
every `dstrack diff` pay for a feature most runs do not use, and would turn
|
|
215
|
+
a missing extra into an import error on an unrelated command.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
result: The comparison to report.
|
|
219
|
+
path: Where to write the PDF.
|
|
220
|
+
quiet: Suppress the success message, for a run whose stdout is data.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Whether the report was written.
|
|
224
|
+
"""
|
|
225
|
+
from dstrack._diff_report import write_pdf_report
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
write_pdf_report(result, path, now=datetime.now(UTC))
|
|
229
|
+
except (MissingDependencyError, OSError) as e:
|
|
230
|
+
console.error(str(e))
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
if not quiet:
|
|
234
|
+
console.success(f"Wrote the comparison report to {path}")
|
|
235
|
+
return True
|