aspect-data 0.0.5__tar.gz → 0.0.6__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.
- {aspect_data-0.0.5 → aspect_data-0.0.6}/PKG-INFO +1 -1
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/data.py +54 -13
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/io.py +128 -33
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/PKG-INFO +1 -1
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/SOURCES.txt +2 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/pyproject.toml +1 -1
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_checkpoint.py +102 -0
- aspect_data-0.0.6/test/test_checksum.py +101 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_data_source.py +4 -1
- aspect_data-0.0.6/test/test_empty_pipeline.py +33 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_io.py +5 -2
- {aspect_data-0.0.5 → aspect_data-0.0.6}/LICENSE +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/README.md +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/__init__.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/checkpoint_utils.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/__init__.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/cli.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/featurize.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/io.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/__init__.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/base.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/chemprop.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/package_data.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/__init__.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/base.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/chemprop.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/deep_functions.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/functions.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/registry.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/typing.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/dependency_links.txt +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/entry_points.txt +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/requires.txt +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/top_level.txt +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/setup.cfg +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_collate.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_collate_chemprop.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_dataloader.py +0 -0
- {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_transform_dict.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
from collections.abc import Callable, Iterable, Mapping
|
|
5
|
+
from copy import copy
|
|
5
6
|
from functools import cached_property, partial
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
@@ -105,6 +106,8 @@ def _fill_na(
|
|
|
105
106
|
"""
|
|
106
107
|
for key in x:
|
|
107
108
|
this_type = types[key]
|
|
109
|
+
if this_type is None:
|
|
110
|
+
continue
|
|
108
111
|
if this_type.startswith(("int", "uint")):
|
|
109
112
|
fill_value = 0
|
|
110
113
|
elif this_type.startswith("float"):
|
|
@@ -311,7 +314,10 @@ class DataPipeline:
|
|
|
311
314
|
return config
|
|
312
315
|
|
|
313
316
|
def clone(self):
|
|
314
|
-
return type(self).from_config(
|
|
317
|
+
return type(self).from_config(
|
|
318
|
+
self.to_config(),
|
|
319
|
+
cache_dir=self.cache_dir,
|
|
320
|
+
)
|
|
315
321
|
|
|
316
322
|
@cached_property
|
|
317
323
|
def column_transforms_serialized(self):
|
|
@@ -505,13 +511,19 @@ class DataPipeline:
|
|
|
505
511
|
keep_extra_columns: Iterable[str] | None = None
|
|
506
512
|
):
|
|
507
513
|
data_in = self._resolve_data(dataset)
|
|
514
|
+
|
|
515
|
+
if len(self.column_transforms) == 0:
|
|
516
|
+
# fast path if empty pipeline
|
|
517
|
+
self.data_in = data_in
|
|
518
|
+
self.data_out = copy(data_in)
|
|
519
|
+
self._data_loaded = True
|
|
520
|
+
self._inspect_data_out()
|
|
521
|
+
return self.data_out
|
|
522
|
+
|
|
508
523
|
input_columns = sorted(set(
|
|
509
|
-
seq[0].input_column for
|
|
524
|
+
seq[0].input_column for _, seq in self.column_transforms.items()
|
|
510
525
|
)) # get only the input column for each branch
|
|
511
526
|
output_columns = sorted(set(self.column_transforms))
|
|
512
|
-
|
|
513
|
-
if len(input_columns) == 0:
|
|
514
|
-
raise AttributeError("No input columns specified.")
|
|
515
527
|
|
|
516
528
|
_check_column_presence(
|
|
517
529
|
input_columns,
|
|
@@ -533,14 +545,21 @@ class DataPipeline:
|
|
|
533
545
|
all_input_columns = list(data_in.column_names)
|
|
534
546
|
all_output_columns = all_input_columns + output_columns
|
|
535
547
|
|
|
548
|
+
def _check_type(f):
|
|
549
|
+
if hasattr(f, "dtype"):
|
|
550
|
+
return f.dtype
|
|
551
|
+
elif hasattr(f, "feature"):
|
|
552
|
+
return _check_type(f.feature)
|
|
553
|
+
else:
|
|
554
|
+
return None
|
|
555
|
+
|
|
536
556
|
data_out = (
|
|
537
557
|
data_in
|
|
538
558
|
.map(
|
|
539
559
|
_fill_na,
|
|
540
560
|
fn_kwargs={
|
|
541
561
|
"types": {
|
|
542
|
-
key:
|
|
543
|
-
else f.feature.dtype
|
|
562
|
+
key: _check_type(f)
|
|
544
563
|
for key, f in data_in.info.features.items()
|
|
545
564
|
},
|
|
546
565
|
},
|
|
@@ -590,7 +609,7 @@ class DataPipeline:
|
|
|
590
609
|
==========
|
|
591
610
|
path
|
|
592
611
|
Checkpoint directory.
|
|
593
|
-
|
|
612
|
+
save_transformed_columns
|
|
594
613
|
Processed columns that must be retained with the checkpoint.
|
|
595
614
|
This is intended for downstream methods that require access to
|
|
596
615
|
exact training representations.
|
|
@@ -603,8 +622,9 @@ class DataPipeline:
|
|
|
603
622
|
``True`` always packages the resolved input data.
|
|
604
623
|
|
|
605
624
|
``False`` never packages it.
|
|
606
|
-
|
|
607
|
-
|
|
625
|
+
discard_example_data
|
|
626
|
+
Normally, save one processed example when available. Set
|
|
627
|
+
discard_example_data=True to turn off.
|
|
608
628
|
"""
|
|
609
629
|
path = Path(path)
|
|
610
630
|
path.mkdir(parents=True, exist_ok=True)
|
|
@@ -741,14 +761,36 @@ class DataPipeline:
|
|
|
741
761
|
path / data_filename,
|
|
742
762
|
cache_dir=pipeline.cache_dir,
|
|
743
763
|
)
|
|
764
|
+
|
|
744
765
|
elif (
|
|
745
766
|
pipeline.data_source is not None
|
|
746
767
|
and pipeline.data_source.is_remote
|
|
747
768
|
):
|
|
748
|
-
source = pipeline.data_source
|
|
749
769
|
pipeline.data_in = autoload(
|
|
750
770
|
pipeline.data_source.uri,
|
|
751
|
-
|
|
771
|
+
cache_dir=pipeline.cache_dir,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
elif (
|
|
775
|
+
pipeline.data_source is not None
|
|
776
|
+
and pipeline.data_source.uri is not None
|
|
777
|
+
and not pipeline.data_source.is_remote
|
|
778
|
+
):
|
|
779
|
+
source = pipeline.data_source
|
|
780
|
+
|
|
781
|
+
if not os.path.exists(
|
|
782
|
+
source.uri
|
|
783
|
+
):
|
|
784
|
+
raise FileNotFoundError(
|
|
785
|
+
"Checkpoint references external training data "
|
|
786
|
+
f"at {source.uri!r}, but that source no longer exists."
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
source.assert_verified()
|
|
790
|
+
|
|
791
|
+
pipeline.data_in = autoload(
|
|
792
|
+
source.uri,
|
|
793
|
+
cache_dir=pipeline.cache_dir,
|
|
752
794
|
)
|
|
753
795
|
|
|
754
796
|
if retained_filename is not None:
|
|
@@ -860,7 +902,6 @@ class DataPipeline:
|
|
|
860
902
|
"No processed dataset available. "
|
|
861
903
|
"Provide `dataset` or call the pipeline first with pipeline(data)."
|
|
862
904
|
)
|
|
863
|
-
|
|
864
905
|
|
|
865
906
|
return DataLoader(
|
|
866
907
|
dataset,
|
|
@@ -64,6 +64,68 @@ def autoload(
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def file_checksum(
|
|
68
|
+
filename: str | os.PathLike,
|
|
69
|
+
chunk_size: int = 1024 * 1024,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Return SHA-256 checksum of a file."""
|
|
72
|
+
|
|
73
|
+
digest = hashlib.sha256()
|
|
74
|
+
|
|
75
|
+
with open(filename, "rb") as handle:
|
|
76
|
+
while chunk := handle.read(chunk_size):
|
|
77
|
+
digest.update(chunk)
|
|
78
|
+
|
|
79
|
+
return digest.hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def dataframe_checksum(
|
|
83
|
+
dataframe,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Return a stable SHA-256 checksum of dataframe contents and schema."""
|
|
86
|
+
|
|
87
|
+
import pyarrow as pa
|
|
88
|
+
from pandas.util import hash_pandas_object
|
|
89
|
+
|
|
90
|
+
digest = hashlib.sha256()
|
|
91
|
+
digest.update(
|
|
92
|
+
repr([
|
|
93
|
+
(
|
|
94
|
+
str(column),
|
|
95
|
+
str(dtype),
|
|
96
|
+
)
|
|
97
|
+
for column, dtype
|
|
98
|
+
in dataframe.dtypes.items()
|
|
99
|
+
]).encode()
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
values = (
|
|
103
|
+
hash_pandas_object(
|
|
104
|
+
dataframe,
|
|
105
|
+
index=False,
|
|
106
|
+
)
|
|
107
|
+
.values
|
|
108
|
+
.tobytes()
|
|
109
|
+
)
|
|
110
|
+
except TypeError:
|
|
111
|
+
table = pa.Table.from_pandas(
|
|
112
|
+
dataframe,
|
|
113
|
+
preserve_index=False,
|
|
114
|
+
)
|
|
115
|
+
table = table.replace_schema_metadata(None)
|
|
116
|
+
sink = pa.BufferOutputStream()
|
|
117
|
+
with pa.ipc.new_stream(
|
|
118
|
+
sink,
|
|
119
|
+
table.schema,
|
|
120
|
+
) as writer:
|
|
121
|
+
writer.write_table(table)
|
|
122
|
+
values = sink.getvalue().to_pybytes()
|
|
123
|
+
|
|
124
|
+
digest.update(values)
|
|
125
|
+
|
|
126
|
+
return digest.hexdigest()
|
|
127
|
+
|
|
128
|
+
|
|
67
129
|
@dataclass(frozen=True)
|
|
68
130
|
class DataSource:
|
|
69
131
|
"""Provenance for a resolved dataset.
|
|
@@ -71,17 +133,23 @@ class DataSource:
|
|
|
71
133
|
Parameters
|
|
72
134
|
----------
|
|
73
135
|
uri
|
|
74
|
-
|
|
136
|
+
Resolved source URI or absolute local filename, when available.
|
|
137
|
+
requested_uri
|
|
138
|
+
Source URI requested by the caller before resolution.
|
|
75
139
|
revision
|
|
76
|
-
Immutable resolved source revision, when available.
|
|
140
|
+
Immutable resolved remote source revision, when available.
|
|
77
141
|
requested_revision
|
|
78
142
|
Revision requested by the caller before resolution, when available.
|
|
143
|
+
checksum
|
|
144
|
+
SHA-256 checksum of non-remote source data, when deterministically
|
|
145
|
+
available.
|
|
79
146
|
"""
|
|
80
147
|
|
|
81
148
|
uri: str | None = None
|
|
82
149
|
requested_uri: str | None = None
|
|
83
150
|
revision: str | None = None
|
|
84
151
|
requested_revision: str | None = None
|
|
152
|
+
checksum: str | None = None
|
|
85
153
|
|
|
86
154
|
@classmethod
|
|
87
155
|
def from_config(
|
|
@@ -118,6 +186,32 @@ class DataSource:
|
|
|
118
186
|
self.revision is not None,
|
|
119
187
|
])
|
|
120
188
|
|
|
189
|
+
def verify(self) -> bool | None:
|
|
190
|
+
"""Verify local source contents against recorded provenance.
|
|
191
|
+
|
|
192
|
+
Returns None when verification is not available.
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
self.checksum is None
|
|
198
|
+
or self.uri is None
|
|
199
|
+
or not os.path.isfile(self.uri)
|
|
200
|
+
):
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
return file_checksum(self.uri) == self.checksum
|
|
204
|
+
|
|
205
|
+
def assert_verified(self) -> None:
|
|
206
|
+
"""Raise if a verifiable source no longer matches its provenance."""
|
|
207
|
+
|
|
208
|
+
if self.verify() is False:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"Data source checksum does not match "
|
|
211
|
+
"the recorded training-data provenance: "
|
|
212
|
+
f"{self.uri!r}: ({self.checksum=} != {file_checksum(self.uri)=})."
|
|
213
|
+
)
|
|
214
|
+
|
|
121
215
|
|
|
122
216
|
def hasher(
|
|
123
217
|
s: str | bytes,
|
|
@@ -208,9 +302,9 @@ def _load_from_file(
|
|
|
208
302
|
def _load_from_dataframe(
|
|
209
303
|
dataframe: DataFrame | Mapping[str, ArrayLike],
|
|
210
304
|
cache: str | None = None,
|
|
211
|
-
) -> Dataset:
|
|
305
|
+
) -> tuple[Dataset, DataSource]:
|
|
212
306
|
|
|
213
|
-
cache,
|
|
307
|
+
cache, _, _ = configure_hf_cache(cache)
|
|
214
308
|
from datasets import Dataset
|
|
215
309
|
from filelock import FileLock
|
|
216
310
|
from pandas import DataFrame
|
|
@@ -219,44 +313,38 @@ def _load_from_dataframe(
|
|
|
219
313
|
if not isinstance(dataframe, DataFrame):
|
|
220
314
|
dataframe = DataFrame(dataframe)
|
|
221
315
|
|
|
222
|
-
|
|
223
|
-
fingerprint
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
for col, dtype in dataframe.dtypes.items()
|
|
227
|
-
]).encode()
|
|
228
|
-
)
|
|
229
|
-
fingerprint.update(
|
|
230
|
-
dataframe.to_string(index=False).encode()
|
|
316
|
+
checksum = dataframe_checksum(dataframe)
|
|
317
|
+
fingerprint = checksum[:16]
|
|
318
|
+
source = DataSource(
|
|
319
|
+
checksum=checksum,
|
|
231
320
|
)
|
|
232
|
-
fingerprint = fingerprint.hexdigest()[:16]
|
|
233
321
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if os.path.exists(
|
|
322
|
+
dataframe_dir = os.path.join(cache, "dataframes")
|
|
323
|
+
filename = f"{fingerprint}.parquet"
|
|
324
|
+
_path = os.path.join(dataframe_dir, filename)
|
|
325
|
+
if os.path.exists(_path):
|
|
238
326
|
return _load_from_file(
|
|
239
|
-
filename=
|
|
240
|
-
cache=
|
|
241
|
-
)
|
|
327
|
+
filename=_path,
|
|
328
|
+
cache=cache,
|
|
329
|
+
), source
|
|
242
330
|
|
|
243
331
|
lockfile = _lock_path(
|
|
244
|
-
key=f"dataframe::{
|
|
245
|
-
cache_dir=
|
|
332
|
+
key=f"dataframe::{checksum}",
|
|
333
|
+
cache_dir=dataframe_dir,
|
|
246
334
|
)
|
|
247
335
|
with FileLock(lockfile, timeout=60. * 60.):
|
|
248
|
-
if os.path.exists(
|
|
336
|
+
if os.path.exists(_path):
|
|
249
337
|
return _load_from_file(
|
|
250
|
-
filename=
|
|
251
|
-
cache=
|
|
338
|
+
filename=_path,
|
|
339
|
+
cache=cache,
|
|
252
340
|
)
|
|
253
|
-
dataframe.to_parquet(
|
|
341
|
+
dataframe.to_parquet(_path, index=False)
|
|
254
342
|
ds = _load_from_file(
|
|
255
|
-
filename=
|
|
256
|
-
cache=
|
|
343
|
+
filename=_path,
|
|
344
|
+
cache=cache,
|
|
257
345
|
)
|
|
258
346
|
|
|
259
|
-
return ds
|
|
347
|
+
return ds, source
|
|
260
348
|
|
|
261
349
|
|
|
262
350
|
def _get_ref_chunk(
|
|
@@ -372,11 +460,10 @@ class AutoDataset:
|
|
|
372
460
|
dataset = data
|
|
373
461
|
source = None
|
|
374
462
|
elif isinstance(data, (DataFrame, Mapping)):
|
|
375
|
-
dataset = _load_from_dataframe(
|
|
463
|
+
dataset, source = _load_from_dataframe(
|
|
376
464
|
data,
|
|
377
465
|
cache=cache,
|
|
378
466
|
)
|
|
379
|
-
source = None
|
|
380
467
|
elif isinstance(data, str):
|
|
381
468
|
if data.startswith("hf://"):
|
|
382
469
|
dataset, source = _resolve_hf_hub_dataset(
|
|
@@ -389,7 +476,15 @@ class AutoDataset:
|
|
|
389
476
|
filename,
|
|
390
477
|
cache=cache,
|
|
391
478
|
)
|
|
392
|
-
|
|
479
|
+
checksum = (
|
|
480
|
+
file_checksum(filename)
|
|
481
|
+
if os.path.isfile(filename)
|
|
482
|
+
else None
|
|
483
|
+
)
|
|
484
|
+
source = DataSource(
|
|
485
|
+
uri=filename,
|
|
486
|
+
checksum=checksum,
|
|
487
|
+
)
|
|
393
488
|
else:
|
|
394
489
|
raise ValueError(
|
|
395
490
|
f"""
|
|
@@ -27,9 +27,11 @@ aspect_data.egg-info/entry_points.txt
|
|
|
27
27
|
aspect_data.egg-info/requires.txt
|
|
28
28
|
aspect_data.egg-info/top_level.txt
|
|
29
29
|
test/test_checkpoint.py
|
|
30
|
+
test/test_checksum.py
|
|
30
31
|
test/test_collate.py
|
|
31
32
|
test/test_collate_chemprop.py
|
|
32
33
|
test/test_data_source.py
|
|
33
34
|
test/test_dataloader.py
|
|
35
|
+
test/test_empty_pipeline.py
|
|
34
36
|
test/test_io.py
|
|
35
37
|
test/test_transform_dict.py
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
3
6
|
from aspect.data import DataPipeline
|
|
4
7
|
from aspect.io import DataSource
|
|
5
8
|
|
|
@@ -133,8 +136,107 @@ def test_remote_source_is_reference_only(tmp_path):
|
|
|
133
136
|
config = json.load(file)
|
|
134
137
|
|
|
135
138
|
assert config["source"] == {
|
|
139
|
+
"checksum": None,
|
|
136
140
|
"uri": "hf://datasets/example/data@abc123:train",
|
|
137
141
|
"requested_uri": "hf://datasets/example/data@main:train",
|
|
138
142
|
"revision": "abc123",
|
|
139
143
|
"requested_revision": "main",
|
|
140
144
|
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_checkpoint_reconstructs_verified_local_source(tmp_path):
|
|
148
|
+
|
|
149
|
+
source = tmp_path / "train.parquet"
|
|
150
|
+
|
|
151
|
+
pd.DataFrame({
|
|
152
|
+
"x": [1., 2., 3.],
|
|
153
|
+
"y": [2., 4., 6.],
|
|
154
|
+
}).to_parquet(
|
|
155
|
+
source,
|
|
156
|
+
index=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
pipeline = DataPipeline()
|
|
160
|
+
pipeline(str(source))
|
|
161
|
+
|
|
162
|
+
checkpoint = tmp_path / "pipeline"
|
|
163
|
+
|
|
164
|
+
pipeline.save(
|
|
165
|
+
checkpoint,
|
|
166
|
+
save_source_data=False,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
restored = DataPipeline.load(checkpoint)
|
|
170
|
+
|
|
171
|
+
assert restored.data_in is not None
|
|
172
|
+
assert restored.data_source.verify() is True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_checkpoint_rejects_modified_local_source(tmp_path):
|
|
176
|
+
|
|
177
|
+
source = tmp_path / "train.parquet"
|
|
178
|
+
|
|
179
|
+
pd.DataFrame({
|
|
180
|
+
"x": [1., 2., 3.],
|
|
181
|
+
}).to_parquet(
|
|
182
|
+
source,
|
|
183
|
+
index=False,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
pipeline = DataPipeline()
|
|
187
|
+
pipeline(str(source))
|
|
188
|
+
|
|
189
|
+
checkpoint = tmp_path / "pipeline"
|
|
190
|
+
|
|
191
|
+
pipeline.save(
|
|
192
|
+
checkpoint,
|
|
193
|
+
save_source_data=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
pd.DataFrame({
|
|
197
|
+
"x": [1., 2., 4.],
|
|
198
|
+
}).to_parquet(
|
|
199
|
+
source,
|
|
200
|
+
index=False,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
with pytest.raises(
|
|
204
|
+
ValueError,
|
|
205
|
+
match="checksum",
|
|
206
|
+
):
|
|
207
|
+
DataPipeline.load(checkpoint)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_embedded_checkpoint_ignores_modified_original_source(
|
|
211
|
+
tmp_path,
|
|
212
|
+
):
|
|
213
|
+
|
|
214
|
+
source = tmp_path / "train.parquet"
|
|
215
|
+
|
|
216
|
+
pd.DataFrame({
|
|
217
|
+
"x": [1., 2., 3.],
|
|
218
|
+
}).to_parquet(
|
|
219
|
+
source,
|
|
220
|
+
index=False,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
pipeline = DataPipeline()
|
|
224
|
+
pipeline(str(source))
|
|
225
|
+
|
|
226
|
+
checkpoint = tmp_path / "pipeline"
|
|
227
|
+
|
|
228
|
+
pipeline.save(
|
|
229
|
+
checkpoint,
|
|
230
|
+
save_source_data=True,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
pd.DataFrame({
|
|
234
|
+
"x": [9., 9., 9.],
|
|
235
|
+
}).to_parquet(
|
|
236
|
+
source,
|
|
237
|
+
index=False,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
restored = DataPipeline.load(checkpoint)
|
|
241
|
+
|
|
242
|
+
assert restored.data_in["x"] == [1., 2., 3.]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from aspect.io import AutoDataset, dataframe_checksum, file_checksum
|
|
5
|
+
|
|
6
|
+
def test_dataframe_checksum_is_stable():
|
|
7
|
+
|
|
8
|
+
from pandas import DataFrame
|
|
9
|
+
|
|
10
|
+
data = DataFrame({
|
|
11
|
+
"x": [1, 2, 3],
|
|
12
|
+
"y": ["a", "b", "c"],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
assert dataframe_checksum(data) == dataframe_checksum(data.copy())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_dataframe_checksum_changes_with_schema():
|
|
19
|
+
|
|
20
|
+
from pandas import DataFrame
|
|
21
|
+
|
|
22
|
+
left = DataFrame({
|
|
23
|
+
"x": [1, 2, 3],
|
|
24
|
+
})
|
|
25
|
+
right = DataFrame({
|
|
26
|
+
"z": [1, 2, 3],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
assert dataframe_checksum(left) != dataframe_checksum(right)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_local_source_checksum(tmp_path):
|
|
33
|
+
|
|
34
|
+
filename = tmp_path / "data.csv"
|
|
35
|
+
filename.write_text("x\n1\n2\n3\n")
|
|
36
|
+
|
|
37
|
+
loaded = AutoDataset.load(str(filename))
|
|
38
|
+
|
|
39
|
+
assert loaded.source.checksum == file_checksum(filename)
|
|
40
|
+
assert loaded.source.verify() is True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_local_source_verification_detects_change(tmp_path):
|
|
44
|
+
|
|
45
|
+
filename = tmp_path / "data.csv"
|
|
46
|
+
filename.write_text(
|
|
47
|
+
"x\n1\n2\n3\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
loaded = AutoDataset.load(str(filename))
|
|
51
|
+
filename.write_text(
|
|
52
|
+
"x\n1\n2\n4\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert loaded.source.verify() is False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_dataframe_source_has_checksum():
|
|
59
|
+
|
|
60
|
+
data = {
|
|
61
|
+
"x": [1, 2, 3],
|
|
62
|
+
}
|
|
63
|
+
loaded = AutoDataset.load(data)
|
|
64
|
+
|
|
65
|
+
assert loaded.source.checksum is not None
|
|
66
|
+
assert len(loaded.source.checksum) == 64
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_dataframe_cache_does_not_nest_dataset_cache(tmp_path):
|
|
70
|
+
|
|
71
|
+
AutoDataset.load(
|
|
72
|
+
{
|
|
73
|
+
"x": [1, 2, 3],
|
|
74
|
+
},
|
|
75
|
+
cache=str(tmp_path),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert os.environ["HF_HOME"] == str(tmp_path)
|
|
79
|
+
assert os.environ["HF_DATASETS_CACHE"] == str(tmp_path / "datasets")
|
|
80
|
+
assert not (tmp_path / "datasets" / "datasets").exists()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_dataframe_checksum_nested_values():
|
|
84
|
+
|
|
85
|
+
from pandas import DataFrame
|
|
86
|
+
|
|
87
|
+
left = DataFrame({
|
|
88
|
+
"x": [
|
|
89
|
+
[1., 2.],
|
|
90
|
+
[3., 4.],
|
|
91
|
+
],
|
|
92
|
+
"y": [1., 2.],
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
right = left.copy(deep=True)
|
|
96
|
+
|
|
97
|
+
assert dataframe_checksum(left) == dataframe_checksum(right)
|
|
98
|
+
|
|
99
|
+
right.at[1, "x"] = [3., 5.]
|
|
100
|
+
|
|
101
|
+
assert dataframe_checksum(left) != dataframe_checksum(right)
|
|
@@ -16,4 +16,7 @@ def test_pipeline_retains_data_source(tmp_path):
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
assert isinstance(pipeline.data_source, DataSource)
|
|
19
|
-
assert pipeline.data_source
|
|
19
|
+
assert pipeline.data_source.uri is None
|
|
20
|
+
assert pipeline.data_source.revision is None
|
|
21
|
+
assert pipeline.data_source.checksum is not None
|
|
22
|
+
assert len(pipeline.data_source.checksum) == 64
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from aspect import DataPipeline
|
|
2
|
+
|
|
3
|
+
def test_empty_pipeline_is_identity():
|
|
4
|
+
|
|
5
|
+
data = {
|
|
6
|
+
"x": [
|
|
7
|
+
[0.],
|
|
8
|
+
[1.],
|
|
9
|
+
],
|
|
10
|
+
"label": [0., 1.],
|
|
11
|
+
}
|
|
12
|
+
pipeline = DataPipeline()
|
|
13
|
+
observed = pipeline(data)
|
|
14
|
+
|
|
15
|
+
assert observed.column_names == ["x", "label"]
|
|
16
|
+
assert observed["x"] == data["x"]
|
|
17
|
+
assert observed["label"] == data["label"]
|
|
18
|
+
assert pipeline.data_out is not pipeline.data_in
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_empty_pipeline_does_not_drop_columns():
|
|
22
|
+
|
|
23
|
+
pipeline = DataPipeline()
|
|
24
|
+
data = {
|
|
25
|
+
"x": [1., 2.],
|
|
26
|
+
"y": [3., 4.],
|
|
27
|
+
}
|
|
28
|
+
observed = pipeline(
|
|
29
|
+
data,
|
|
30
|
+
drop_unused_columns=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert set(observed.column_names) == set(data)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from aspect.io import AutoDataset, DataSource
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def
|
|
4
|
+
def test_mapping_source_records_checksum(tmp_path):
|
|
5
5
|
resolved = AutoDataset.load(
|
|
6
6
|
{
|
|
7
7
|
"x": [
|
|
@@ -14,7 +14,10 @@ def test_mapping_source_is_ephemeral(tmp_path):
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
assert isinstance(resolved.source, DataSource)
|
|
17
|
-
assert resolved.source
|
|
17
|
+
assert resolved.source.uri is None
|
|
18
|
+
assert resolved.source.revision is None
|
|
19
|
+
assert resolved.source.checksum is not None
|
|
20
|
+
assert len(resolved.source.checksum) == 64
|
|
18
21
|
assert len(resolved._dataset) == 2
|
|
19
22
|
|
|
20
23
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|