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.
Files changed (48) hide show
  1. {dstrack-0.1.0 → dstrack-0.2.0}/PKG-INFO +35 -5
  2. {dstrack-0.1.0 → dstrack-0.2.0}/README.md +32 -4
  3. dstrack-0.2.0/pyproject.toml +124 -0
  4. dstrack-0.1.0/pyproject.toml → dstrack-0.2.0/pyproject.toml.orig +15 -2
  5. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_cli.py +8 -0
  6. dstrack-0.2.0/src/dstrack/_diff.py +235 -0
  7. dstrack-0.2.0/src/dstrack/_diff_render.py +925 -0
  8. dstrack-0.2.0/src/dstrack/_diff_report.py +1020 -0
  9. dstrack-0.2.0/src/dstrack/_log.py +125 -0
  10. dstrack-0.2.0/src/dstrack/_resolve.py +602 -0
  11. dstrack-0.2.0/src/dstrack/assets/diff-report.png +0 -0
  12. dstrack-0.2.0/src/dstrack/assets/dstrack-logo.png +0 -0
  13. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/cache.py +81 -4
  14. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/console.py +10 -0
  15. dstrack-0.2.0/src/dstrack/diff/__init__.py +103 -0
  16. dstrack-0.2.0/src/dstrack/diff/_engine.py +1252 -0
  17. dstrack-0.2.0/src/dstrack/diff/_extract.py +305 -0
  18. dstrack-0.2.0/src/dstrack/diff/_model.py +511 -0
  19. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/errors.py +16 -0
  20. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/schemas/snapshot_v1.json +1 -1
  21. dstrack-0.2.0/src/dstrack/snapshot/_version.py +25 -0
  22. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/store.py +92 -3
  23. dstrack-0.1.0/src/dstrack/_log.py +0 -268
  24. dstrack-0.1.0/src/dstrack/snapshot/_version.py +0 -13
  25. {dstrack-0.1.0 → dstrack-0.2.0}/LICENSE +0 -0
  26. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/__init__.py +0 -0
  27. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/__init__.py +0 -0
  28. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_cli.py +0 -0
  29. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_environment.py +0 -0
  30. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_profiling.py +0 -0
  31. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_report.py +0 -0
  32. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_runner.py +0 -0
  33. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_benchmark/_synthetic.py +0 -0
  34. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_log_render.py +0 -0
  35. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_logging.py +0 -0
  36. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_store_template.py +0 -0
  37. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/_track.py +0 -0
  38. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/paths.py +0 -0
  39. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/__init__.py +0 -0
  40. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_csv.py +0 -0
  41. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_protocol.py +0 -0
  42. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_registry.py +0 -0
  43. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/readers/_resolve.py +0 -0
  44. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/__init__.py +0 -0
  45. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_builder.py +0 -0
  46. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_metadata.py +0 -0
  47. {dstrack-0.1.0 → dstrack-0.2.0}/src/dstrack/snapshot/_stats.py +0 -0
  48. {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.1.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
  [![codecov](https://codecov.io/github/leoyala/dstrack/graph/badge.svg?token=DEKCE4LR88)](https://codecov.io/github/leoyala/dstrack)
30
32
 
31
33
  <p align="center">
32
- <img src="docs/assets/dstrack-logo.png" width="200" alt="dstrack logo">
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
- Change detection and drift monitoring are on the way - comparing snapshots, surfacing
122
- schema and distribution shifts, and failing CI on drift. See the
123
- [roadmap](docs/roadmap.md) for what's planned.
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
  [![codecov](https://codecov.io/github/leoyala/dstrack/graph/badge.svg?token=DEKCE4LR88)](https://codecov.io/github/leoyala/dstrack)
5
5
 
6
6
  <p align="center">
7
- <img src="docs/assets/dstrack-logo.png" width="200" alt="dstrack logo">
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
- Change detection and drift monitoring are on the way - comparing snapshots, surfacing
97
- schema and distribution shifts, and failing CI on drift. See the
98
- [roadmap](docs/roadmap.md) for what's planned.
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.1.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.12.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