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,196 @@
|
|
|
1
|
+
"""Runs the snapshot pipeline against a synthetic CSV and times each phase.
|
|
2
|
+
|
|
3
|
+
The runner performs no output of its own: progress is announced through a
|
|
4
|
+
[BenchmarkObserver][dstrack._benchmark._runner.BenchmarkObserver] and everything
|
|
5
|
+
measured is returned as a
|
|
6
|
+
[BenchmarkRun][dstrack._benchmark._runner.BenchmarkRun], so the same run can be
|
|
7
|
+
driven from a test with no console attached.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import cProfile
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Protocol
|
|
17
|
+
|
|
18
|
+
from dstrack._benchmark._environment import EnvironmentInfo, collect_environment_info
|
|
19
|
+
from dstrack._benchmark._profiling import CallGraph, dstrack_scope
|
|
20
|
+
from dstrack._benchmark._synthetic import SyntheticCsvSpec, write_synthetic_csv
|
|
21
|
+
from dstrack.readers import CsvReader
|
|
22
|
+
from dstrack.snapshot import MetadataBuilder, StatsComputer
|
|
23
|
+
|
|
24
|
+
_DATASET_NAME = "benchmark-dataset"
|
|
25
|
+
_CREATED_BY = "dstrack-benchmark"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class BenchmarkResult:
|
|
30
|
+
"""Timings and sizing for one synthetic-CSV snapshot benchmark run.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
num_rows: Rows written to the synthetic CSV.
|
|
34
|
+
num_rows_read: Rows the statistics pass actually read back.
|
|
35
|
+
num_columns: Columns the reader inferred, including ``id``.
|
|
36
|
+
csv_size_bytes: Size of the generated CSV on disk.
|
|
37
|
+
generation_seconds: Time to write the synthetic CSV.
|
|
38
|
+
metadata_seconds: Time to build snapshot metadata from the reader.
|
|
39
|
+
stats_seconds: Time for the statistics data pass.
|
|
40
|
+
schema_hash: Schema hash of the snapshot that was built.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
num_rows: int
|
|
44
|
+
num_rows_read: int
|
|
45
|
+
num_columns: int
|
|
46
|
+
csv_size_bytes: int
|
|
47
|
+
generation_seconds: float
|
|
48
|
+
metadata_seconds: float
|
|
49
|
+
stats_seconds: float
|
|
50
|
+
schema_hash: str
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def total_snapshot_seconds(self) -> float:
|
|
54
|
+
"""Time to build the snapshot: metadata plus statistics."""
|
|
55
|
+
return self.metadata_seconds + self.stats_seconds
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def rows_per_second(self) -> float:
|
|
59
|
+
"""Rows processed per second of snapshot time."""
|
|
60
|
+
return self.num_rows_read / self.total_snapshot_seconds
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class BenchmarkRun:
|
|
65
|
+
"""Everything one benchmark run produced, ready to be reported."""
|
|
66
|
+
|
|
67
|
+
result: BenchmarkResult
|
|
68
|
+
environment: EnvironmentInfo
|
|
69
|
+
call_graph: CallGraph | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BenchmarkObserver(Protocol):
|
|
73
|
+
"""Receives a benchmark's progress as each phase starts."""
|
|
74
|
+
|
|
75
|
+
def generating_csv(self, path: Path, num_rows: int) -> None:
|
|
76
|
+
"""Called before the synthetic CSV is written."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def building_metadata(self) -> None:
|
|
80
|
+
"""Called before snapshot metadata is built."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def computing_stats(self) -> None:
|
|
84
|
+
"""Called before the statistics data pass."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SilentObserver:
|
|
89
|
+
"""Observer that reports nothing; the default for programmatic runs."""
|
|
90
|
+
|
|
91
|
+
def generating_csv(self, path: Path, num_rows: int) -> None:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def building_metadata(self) -> None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def computing_stats(self) -> None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class _Elapsed:
|
|
103
|
+
"""Wall-clock seconds a phase took, filled in when its block exits."""
|
|
104
|
+
|
|
105
|
+
seconds: float = 0.0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class BenchmarkRunner:
|
|
109
|
+
"""Generates a synthetic CSV and times a snapshot being built from it.
|
|
110
|
+
|
|
111
|
+
Metadata and statistics are timed separately, and share one profiler so the
|
|
112
|
+
call tree covers the whole snapshot pipeline. CSV generation is never
|
|
113
|
+
profiled: it measures the harness, not dstrack.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
spec: The synthetic dataset to benchmark against.
|
|
117
|
+
observer: Notified as each phase starts. Defaults to reporting nothing.
|
|
118
|
+
profile: Whether to profile dstrack's own methods while building the
|
|
119
|
+
snapshot. Adds cProfile overhead to the reported timings.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
spec: SyntheticCsvSpec,
|
|
125
|
+
*,
|
|
126
|
+
observer: BenchmarkObserver | None = None,
|
|
127
|
+
profile: bool = True,
|
|
128
|
+
) -> None:
|
|
129
|
+
self._spec = spec
|
|
130
|
+
self._observer = observer or SilentObserver()
|
|
131
|
+
self._profiler = cProfile.Profile() if profile else None
|
|
132
|
+
|
|
133
|
+
def run(self, csv_path: Path) -> BenchmarkRun:
|
|
134
|
+
"""Generate the dataset at ``csv_path`` and benchmark a snapshot of it."""
|
|
135
|
+
# clean profiler if already created to avoid misleading profiles on re-runs.
|
|
136
|
+
if self._profiler is not None:
|
|
137
|
+
self._profiler.clear()
|
|
138
|
+
|
|
139
|
+
# generate data for benchmark
|
|
140
|
+
self._observer.generating_csv(csv_path, self._spec.num_rows)
|
|
141
|
+
with self._timed() as generation:
|
|
142
|
+
write_synthetic_csv(csv_path, self._spec)
|
|
143
|
+
csv_size_bytes = csv_path.stat().st_size
|
|
144
|
+
|
|
145
|
+
reader = CsvReader(csv_path)
|
|
146
|
+
|
|
147
|
+
self._observer.building_metadata()
|
|
148
|
+
with self._timed(profiled=True) as metadata_phase:
|
|
149
|
+
metadata = MetadataBuilder().build(
|
|
150
|
+
reader,
|
|
151
|
+
dataset_name=_DATASET_NAME,
|
|
152
|
+
dataset_path=csv_path,
|
|
153
|
+
source_type="file",
|
|
154
|
+
created_by=_CREATED_BY,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self._observer.computing_stats()
|
|
158
|
+
with self._timed(profiled=True) as stats_phase:
|
|
159
|
+
stats = StatsComputer().compute(reader)
|
|
160
|
+
|
|
161
|
+
result = BenchmarkResult(
|
|
162
|
+
num_rows=self._spec.num_rows,
|
|
163
|
+
num_rows_read=stats.num_rows,
|
|
164
|
+
num_columns=metadata.num_columns,
|
|
165
|
+
csv_size_bytes=csv_size_bytes,
|
|
166
|
+
generation_seconds=generation.seconds,
|
|
167
|
+
metadata_seconds=metadata_phase.seconds,
|
|
168
|
+
stats_seconds=stats_phase.seconds,
|
|
169
|
+
schema_hash=metadata.schema_hash,
|
|
170
|
+
)
|
|
171
|
+
return BenchmarkRun(
|
|
172
|
+
result=result,
|
|
173
|
+
environment=collect_environment_info(),
|
|
174
|
+
call_graph=self._call_graph(),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@contextmanager
|
|
178
|
+
def _timed(self, *, profiled: bool = False) -> Iterator[_Elapsed]:
|
|
179
|
+
"""Time the block, optionally recording it into the shared profiler."""
|
|
180
|
+
profiler = self._profiler if profiled else None
|
|
181
|
+
elapsed = _Elapsed()
|
|
182
|
+
if profiler is not None:
|
|
183
|
+
profiler.enable()
|
|
184
|
+
start = time.perf_counter()
|
|
185
|
+
try:
|
|
186
|
+
yield elapsed
|
|
187
|
+
finally:
|
|
188
|
+
elapsed.seconds = time.perf_counter() - start
|
|
189
|
+
if profiler is not None:
|
|
190
|
+
profiler.disable()
|
|
191
|
+
|
|
192
|
+
def _call_graph(self) -> CallGraph | None:
|
|
193
|
+
"""Get a call graph of the snapshot phases, or None if profiling was disabled."""
|
|
194
|
+
if self._profiler is None:
|
|
195
|
+
return None
|
|
196
|
+
return CallGraph.from_profile(self._profiler, dstrack_scope())
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Synthetic CSV generation for the benchmark.
|
|
2
|
+
|
|
3
|
+
The dataset is described by a
|
|
4
|
+
[SyntheticCsvSpec][dstrack._benchmark._synthetic.SyntheticCsvSpec] value object
|
|
5
|
+
and written by
|
|
6
|
+
[write_synthetic_csv()][dstrack._benchmark._synthetic.write_synthetic_csv]. Each
|
|
7
|
+
generated dtype is one
|
|
8
|
+
[SyntheticColumn][dstrack._benchmark._synthetic.SyntheticColumn] whose
|
|
9
|
+
[generate][dstrack._benchmark._synthetic.SyntheticColumn.generate] callable
|
|
10
|
+
renders a single cell, so supporting a new dtype means adding one cell factory
|
|
11
|
+
and one entry to ``_COLUMN_KINDS`` rather than threading another count through
|
|
12
|
+
every layer.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import csv
|
|
16
|
+
import math
|
|
17
|
+
import random
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import UTC, datetime, timedelta
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TypeAlias
|
|
23
|
+
|
|
24
|
+
# Renders one cell as the text that lands in the CSV. Cells are always
|
|
25
|
+
# generated from the spec's seeded RNG, so a spec plus a seed is reproducible.
|
|
26
|
+
CellFactory: TypeAlias = Callable[[random.Random], str]
|
|
27
|
+
|
|
28
|
+
_WORD_POOL: tuple[str, ...] = (
|
|
29
|
+
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel",
|
|
30
|
+
"india", "juliet", "kilo", "lima", "mike", "november", "oscar", "papa",
|
|
31
|
+
"quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey",
|
|
32
|
+
) # fmt: skip
|
|
33
|
+
|
|
34
|
+
_DATETIME_START = datetime(2020, 1, 1, tzinfo=UTC)
|
|
35
|
+
_DATETIME_END = datetime(2026, 1, 1, tzinfo=UTC)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _numeric_cell(rng: random.Random) -> str:
|
|
39
|
+
return f"{rng.gauss(100, 25):.4f}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _string_cell(rng: random.Random) -> str:
|
|
43
|
+
return " ".join(rng.choices(_WORD_POOL, k=rng.randint(1, 4)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _datetime_cell(rng: random.Random) -> str:
|
|
47
|
+
span = (_DATETIME_END - _DATETIME_START).total_seconds()
|
|
48
|
+
return (_DATETIME_START + timedelta(seconds=rng.uniform(0, span))).isoformat()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _bool_cell(rng: random.Random) -> str:
|
|
52
|
+
return rng.choice(["True", "False"])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class SyntheticColumn:
|
|
57
|
+
"""One generated column: its header name and how to render a cell.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
name: Column header written to the CSV.
|
|
61
|
+
generate: Renders a single non-null cell from the seeded RNG.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
name: str
|
|
65
|
+
generate: CellFactory
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# (name prefix, cell factory) per dtype dstrack's CSV reader infers, in the
|
|
69
|
+
# order the columns appear in the file.
|
|
70
|
+
_COLUMN_KINDS: tuple[tuple[str, CellFactory], ...] = (
|
|
71
|
+
("numeric", _numeric_cell),
|
|
72
|
+
("string", _string_cell),
|
|
73
|
+
("datetime", _datetime_cell),
|
|
74
|
+
("bool", _bool_cell),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class SyntheticCsvSpec:
|
|
80
|
+
"""Describes the synthetic dataset a benchmark run should be measured on.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
num_rows: Number of data rows to generate.
|
|
84
|
+
num_numeric_cols: Number of ``float64`` columns.
|
|
85
|
+
num_string_cols: Number of ``string`` columns.
|
|
86
|
+
num_datetime_cols: Number of ``datetime64`` columns.
|
|
87
|
+
num_bool_cols: Number of ``bool`` columns.
|
|
88
|
+
null_rate: Fraction of non-id cells left empty (null).
|
|
89
|
+
seed: Seed for the random generator, for reproducible datasets.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
num_rows: int = 200_000
|
|
93
|
+
num_numeric_cols: int = 5
|
|
94
|
+
num_string_cols: int = 5
|
|
95
|
+
num_datetime_cols: int = 2
|
|
96
|
+
num_bool_cols: int = 2
|
|
97
|
+
null_rate: float = 0.02
|
|
98
|
+
seed: int = 42
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
"""Reject counts and a null rate that would silently corrupt the dataset.
|
|
102
|
+
|
|
103
|
+
Negative counts and an out-of-range ``null_rate`` have no meaningful
|
|
104
|
+
dataset to generate, so they are refused here rather than producing a
|
|
105
|
+
subtly wrong CSV further downstream.
|
|
106
|
+
"""
|
|
107
|
+
for name in (
|
|
108
|
+
"num_rows",
|
|
109
|
+
"num_numeric_cols",
|
|
110
|
+
"num_string_cols",
|
|
111
|
+
"num_datetime_cols",
|
|
112
|
+
"num_bool_cols",
|
|
113
|
+
):
|
|
114
|
+
value: int = getattr(self, name)
|
|
115
|
+
if value < 0:
|
|
116
|
+
raise ValueError(f"{name} must be >= 0, got {value}")
|
|
117
|
+
if not (math.isfinite(self.null_rate) and 0.0 <= self.null_rate <= 1.0):
|
|
118
|
+
raise ValueError(f"null_rate must be in [0, 1], got {self.null_rate!r}")
|
|
119
|
+
|
|
120
|
+
def columns(self) -> list[SyntheticColumn]:
|
|
121
|
+
"""Return the nullable, randomly generated columns, in file order.
|
|
122
|
+
|
|
123
|
+
The leading ``id`` column is not included: it is written from the row
|
|
124
|
+
index and is never null.
|
|
125
|
+
"""
|
|
126
|
+
counts = (
|
|
127
|
+
self.num_numeric_cols,
|
|
128
|
+
self.num_string_cols,
|
|
129
|
+
self.num_datetime_cols,
|
|
130
|
+
self.num_bool_cols,
|
|
131
|
+
)
|
|
132
|
+
return [
|
|
133
|
+
SyntheticColumn(f"{prefix}_{i}", generate)
|
|
134
|
+
for (prefix, generate), count in zip(_COLUMN_KINDS, counts, strict=True)
|
|
135
|
+
for i in range(count)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_synthetic_csv(path: Path, spec: SyntheticCsvSpec) -> None:
|
|
140
|
+
"""Write the dataset described by ``spec`` to ``path``, overwriting it.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
path: Destination CSV path; overwritten if it already exists.
|
|
144
|
+
spec: The dataset to generate.
|
|
145
|
+
"""
|
|
146
|
+
rng = random.Random(spec.seed)
|
|
147
|
+
columns = spec.columns()
|
|
148
|
+
|
|
149
|
+
with path.open("w", newline="", encoding="utf-8") as fh:
|
|
150
|
+
writer = csv.writer(fh)
|
|
151
|
+
writer.writerow(["id", *(column.name for column in columns)])
|
|
152
|
+
for row_id in range(spec.num_rows):
|
|
153
|
+
row = [str(row_id)]
|
|
154
|
+
row.extend(
|
|
155
|
+
"" if rng.random() < spec.null_rate else column.generate(rng)
|
|
156
|
+
for column in columns
|
|
157
|
+
)
|
|
158
|
+
writer.writerow(row)
|
dstrack/_cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from dstrack import __version__, console
|
|
9
|
+
from dstrack._log import log
|
|
10
|
+
from dstrack._store_template import STORE_TEMPLATE, materialize
|
|
11
|
+
from dstrack._track import track
|
|
12
|
+
from dstrack.errors import StoreInitError
|
|
13
|
+
from dstrack.utils import get_invocation_path
|
|
14
|
+
|
|
15
|
+
_log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
epilog="Made with :heart:",
|
|
20
|
+
suggest_commands=True,
|
|
21
|
+
pretty_exceptions_show_locals=False,
|
|
22
|
+
help="Main CLI interface to `dstrack`'s capabilities.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def init_local_store() -> Path:
|
|
27
|
+
"""Initializes local store structure.
|
|
28
|
+
|
|
29
|
+
Builds [STORE_TEMPLATE][dstrack._store_template.STORE_TEMPLATE] in a
|
|
30
|
+
temporary directory next to the destination, and only moves it into place
|
|
31
|
+
once every file and directory in the template has been created successfully.
|
|
32
|
+
The destination is therefore never touched by a failed or partial build.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Path where the local store was created.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
FileExistsError: If the local store path already exists.
|
|
39
|
+
StoreInitError: If building the store from its template fails. The
|
|
40
|
+
temporary build directory is cleaned up before raising.
|
|
41
|
+
"""
|
|
42
|
+
invocation_path = get_invocation_path()
|
|
43
|
+
store_path = invocation_path / STORE_TEMPLATE.name
|
|
44
|
+
|
|
45
|
+
if store_path.is_dir():
|
|
46
|
+
raise FileExistsError(f"Local store path already exists: {store_path}")
|
|
47
|
+
|
|
48
|
+
with tempfile.TemporaryDirectory(
|
|
49
|
+
prefix=f"{STORE_TEMPLATE.name}-tmp-", dir=invocation_path
|
|
50
|
+
) as tmp_dir:
|
|
51
|
+
try:
|
|
52
|
+
built_path = materialize(STORE_TEMPLATE, Path(tmp_dir))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise StoreInitError(
|
|
55
|
+
f"Failed to initialize local store at {store_path}"
|
|
56
|
+
) from e
|
|
57
|
+
|
|
58
|
+
console.info(f"Generating local store structure at {store_path}.")
|
|
59
|
+
built_path.rename(store_path)
|
|
60
|
+
|
|
61
|
+
return store_path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command(
|
|
65
|
+
help="Initialize local store before starting to track any dataset. "
|
|
66
|
+
"The local store `.dstrack/` is created at the location where this "
|
|
67
|
+
"command is called."
|
|
68
|
+
)
|
|
69
|
+
def init(
|
|
70
|
+
allow_exists: Annotated[
|
|
71
|
+
bool, typer.Option(help="Do not fail if `.dstrack/` already exists.")
|
|
72
|
+
] = False,
|
|
73
|
+
) -> None:
|
|
74
|
+
try:
|
|
75
|
+
store_path = init_local_store()
|
|
76
|
+
console.success(f"Finished creating local store: {store_path}")
|
|
77
|
+
except FileExistsError as e:
|
|
78
|
+
if not allow_exists:
|
|
79
|
+
console.error(str(e))
|
|
80
|
+
raise
|
|
81
|
+
_log.warning(f"{e}")
|
|
82
|
+
console.warning(f"{e}")
|
|
83
|
+
except StoreInitError as e:
|
|
84
|
+
_log.error(f"Failed to create local store. {e}")
|
|
85
|
+
console.error(str(e))
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
app.command(help="Compute a snapshot of a dataset and store it in the local store.")(
|
|
90
|
+
track
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
app.command(
|
|
94
|
+
help="Show the snapshot history of a tracked dataset, given its dataset id "
|
|
95
|
+
"or its path."
|
|
96
|
+
)(log)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command(help="Print package version.")
|
|
100
|
+
def version() -> None:
|
|
101
|
+
print(__version__)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
app()
|