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.
@@ -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()