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.
Files changed (39) hide show
  1. {aspect_data-0.0.5 → aspect_data-0.0.6}/PKG-INFO +1 -1
  2. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/data.py +54 -13
  3. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/io.py +128 -33
  4. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/PKG-INFO +1 -1
  5. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/SOURCES.txt +2 -0
  6. {aspect_data-0.0.5 → aspect_data-0.0.6}/pyproject.toml +1 -1
  7. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_checkpoint.py +102 -0
  8. aspect_data-0.0.6/test/test_checksum.py +101 -0
  9. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_data_source.py +4 -1
  10. aspect_data-0.0.6/test/test_empty_pipeline.py +33 -0
  11. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_io.py +5 -2
  12. {aspect_data-0.0.5 → aspect_data-0.0.6}/LICENSE +0 -0
  13. {aspect_data-0.0.5 → aspect_data-0.0.6}/README.md +0 -0
  14. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/__init__.py +0 -0
  15. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/checkpoint_utils.py +0 -0
  16. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/__init__.py +0 -0
  17. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/cli.py +0 -0
  18. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/featurize.py +0 -0
  19. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/cli_module/io.py +0 -0
  20. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/__init__.py +0 -0
  21. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/base.py +0 -0
  22. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/collate/chemprop.py +0 -0
  23. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/package_data.py +0 -0
  24. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/__init__.py +0 -0
  25. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/base.py +0 -0
  26. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/chemprop.py +0 -0
  27. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/deep_functions.py +0 -0
  28. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/functions.py +0 -0
  29. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/transform/registry.py +0 -0
  30. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect/typing.py +0 -0
  31. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/dependency_links.txt +0 -0
  32. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/entry_points.txt +0 -0
  33. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/requires.txt +0 -0
  34. {aspect_data-0.0.5 → aspect_data-0.0.6}/aspect_data.egg-info/top_level.txt +0 -0
  35. {aspect_data-0.0.5 → aspect_data-0.0.6}/setup.cfg +0 -0
  36. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_collate.py +0 -0
  37. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_collate_chemprop.py +0 -0
  38. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_dataloader.py +0 -0
  39. {aspect_data-0.0.5 → aspect_data-0.0.6}/test/test_transform_dict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspect-data
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: General purpose, chemistry, and taxonomic featurization of tabular data with caching.
5
5
  Author-email: Eachan Johnson <eachan.johnson@crick.ac.uk>
6
6
  License: MIT License
@@ -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(self.to_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 k, seq in self.column_transforms.items()
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: f.dtype if hasattr(f, "dtype")
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
- retain_columns
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
- retain_example
607
- Save one processed example when available.
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
- cache=pipeline.cache_dir,
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
- Original source URI or absolute local filename, when available.
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, datasets_cache, _ = configure_hf_cache(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
- fingerprint = hashlib.sha256()
223
- fingerprint.update(
224
- repr([
225
- (str(col), str(dtype))
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
- csv_dir = os.path.join(cache, "dataframes")
235
- csv_filename = f"{fingerprint}.parquet"
236
- csv_path = os.path.join(csv_dir, csv_filename)
237
- if os.path.exists(csv_path):
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=csv_path,
240
- cache=datasets_cache,
241
- )
327
+ filename=_path,
328
+ cache=cache,
329
+ ), source
242
330
 
243
331
  lockfile = _lock_path(
244
- key=f"dataframe::{fingerprint}",
245
- cache_dir=csv_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(csv_path):
336
+ if os.path.exists(_path):
249
337
  return _load_from_file(
250
- filename=csv_path,
251
- cache=datasets_cache,
338
+ filename=_path,
339
+ cache=cache,
252
340
  )
253
- dataframe.to_parquet(csv_path, index=False)
341
+ dataframe.to_parquet(_path, index=False)
254
342
  ds = _load_from_file(
255
- filename=csv_path,
256
- cache=datasets_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
- source = DataSource(uri=filename)
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"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspect-data
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: General purpose, chemistry, and taxonomic featurization of tabular data with caching.
5
5
  Author-email: Eachan Johnson <eachan.johnson@crick.ac.uk>
6
6
  License: MIT License
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "aspect-data"
3
- version = "0.0.5"
3
+ version = "0.0.6"
4
4
  authors = [
5
5
  { name="Eachan Johnson", email="eachan.johnson@crick.ac.uk" },
6
6
  ]
@@ -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 == DataSource()
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 test_mapping_source_is_ephemeral(tmp_path):
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 == DataSource()
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