providence 0.0.1__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.
providence/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from importlib.metadata import version
2
+
3
+ app_name = "providence"
4
+ __author__ = "Eachan Johnson"
5
+ __version__ = version(app_name)
6
+
7
+ from .data import DataResolver
8
+ from .io import DataSource
@@ -0,0 +1,95 @@
1
+ """"Utilities for loading and saving checkpoints."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union
4
+ from tempfile import TemporaryDirectory
5
+ import os
6
+ import json
7
+
8
+ from carabiner import print_err
9
+
10
+ if TYPE_CHECKING:
11
+ from datasets import Dataset, IterableDataset
12
+ else:
13
+ Dataset, IterableDataset = Any, Any
14
+
15
+
16
+ def _load_json(checkpoint: str, filename: str) -> Dict[str, Any]:
17
+ with open(os.path.join(checkpoint, filename), "r") as f:
18
+ obj = json.load(f)
19
+ return obj
20
+
21
+
22
+ def save_json(obj, filename: str) -> None:
23
+ with open(filename, "w") as f:
24
+ try:
25
+ json.dump(obj, f, sort_keys=True, indent=4)
26
+ except TypeError as e:
27
+ print_err(f"{obj=}")
28
+ raise e
29
+ return None
30
+
31
+
32
+ def _load_hf_dataset(checkpoint, filename) -> Union[Dataset, IterableDataset]:
33
+ from datasets import load_from_disk
34
+ return load_from_disk(os.path.join(checkpoint, filename))
35
+
36
+
37
+ FILE_LOADING_CALLBACKS = {
38
+ "json": _load_json,
39
+ "hf-dataset": _load_hf_dataset,
40
+ }
41
+
42
+
43
+ def load_checkpoint_file(
44
+ checkpoint: str,
45
+ filename: str,
46
+ *args,
47
+ callback: Union[str, Callable] = "json",
48
+ none_on_error: bool = False,
49
+ cache_dir: Optional[str] = None,
50
+ **kwargs
51
+ ) -> Union[Any, None]:
52
+ from huggingface_hub import snapshot_download
53
+
54
+ obj = None
55
+ if isinstance(callback, str):
56
+ try:
57
+ callback = FILE_LOADING_CALLBACKS[callback.casefold()]
58
+ except KeyError:
59
+ raise ValueError(
60
+ """
61
+ File loading callback must be callable or name.
62
+ """
63
+ )
64
+ if os.path.exists(checkpoint):
65
+ obj = callback(checkpoint, filename)
66
+ elif checkpoint.startswith("hf://"):
67
+ checkpoint = checkpoint.split("hf://")[-1]
68
+ if filename.endswith(".hf"):
69
+ filename_pattern = [filename + '/*.arrow', filename + '/*.json']
70
+ else:
71
+ filename_pattern = filename
72
+ with TemporaryDirectory() as tmpdirname:
73
+ try:
74
+ print_err(f"Looking up: {checkpoint} :: {filename}")
75
+ snapshot_download(
76
+ repo_id=checkpoint,
77
+ allow_patterns=filename_pattern,
78
+ local_dir=tmpdirname,
79
+ cache_dir=cache_dir,
80
+ *args, **kwargs
81
+ )
82
+ except Exception as e:
83
+ print_err(e)
84
+ if none_on_error:
85
+ return None
86
+ else:
87
+ raise e
88
+ else:
89
+ obj = callback(tmpdirname, filename)
90
+ if obj is not None:
91
+ return obj
92
+ else:
93
+ raise AttributeError(
94
+ f"Could not load anything from {checkpoint=}, {filename=} with {callback=}."
95
+ )
File without changes
@@ -0,0 +1,92 @@
1
+ """Command-line interface for providence."""
2
+
3
+ from argparse import FileType
4
+ import os
5
+ import sys
6
+
7
+ from carabiner.cliutils import CLIOption, CLICommand, CLIApp
8
+
9
+ from .. import app_name, __version__
10
+ from .fetch import _fetch
11
+
12
+ def main() -> None:
13
+
14
+ input_file = CLIOption(
15
+ 'input_file',
16
+ type=FileType('r'),
17
+ default=sys.stdin,
18
+ nargs='?',
19
+ help='Input file. Default: STDIN',
20
+ )
21
+ input_filename = CLIOption(
22
+ 'input_file',
23
+ type=str,
24
+ help='Input file.',
25
+ )
26
+ force_write = CLIOption(
27
+ '--force-write-data', '-f',
28
+ action='store_true',
29
+ help='Save data to disk from remote repos, instead of simply saving a reference.',
30
+ )
31
+ cache = CLIOption(
32
+ '--cache',
33
+ type=str,
34
+ default=None,
35
+ help='Where to cache data.',
36
+ )
37
+ output_name = CLIOption(
38
+ '--output', '-o',
39
+ type=str,
40
+ default=False,
41
+ help='Output filename for data table.',
42
+ )
43
+ checkpoint = CLIOption(
44
+ '--checkpoint', '-c',
45
+ type=str,
46
+ default=False,
47
+ help='Output filename for Providence checkpoint.',
48
+ )
49
+
50
+ # slice dataset
51
+ slice_start = CLIOption(
52
+ '--start',
53
+ type=int,
54
+ default=None,
55
+ help='First row of dataset to process.',
56
+ )
57
+ slice_end = CLIOption(
58
+ '--end',
59
+ type=int,
60
+ default=None,
61
+ help='Last row of dataset to process. Default: end of dataset.',
62
+ )
63
+
64
+ get_command = CLICommand(
65
+ "get",
66
+ description="Load a local or remote dataset, and save as a reloadable and verifiable providence checkpoint.",
67
+ options=[
68
+ input_filename,
69
+ slice_start,
70
+ slice_end,
71
+ force_write,
72
+ checkpoint,
73
+ output_name,
74
+ cache,
75
+ ],
76
+ main=_fetch,
77
+ )
78
+
79
+ app = CLIApp(
80
+ app_name,
81
+ description="Data ingestion, validation, and provenance.",
82
+ version=__version__,
83
+ commands=[
84
+ get_command,
85
+ ],
86
+ )
87
+ app.run()
88
+ return None
89
+
90
+
91
+ if __name__ == '__main__':
92
+ main()
@@ -0,0 +1,43 @@
1
+ from argparse import Namespace
2
+ import os
3
+
4
+ from carabiner.cliutils import clicommand
5
+
6
+
7
+ @clicommand("Fetching data with the following parameters")
8
+ def _fetch(args: Namespace) -> None:
9
+
10
+ from .io import _save_dataset
11
+ from ..data import DataResolver
12
+
13
+ resolved = DataResolver(
14
+ args.input_file,
15
+ cache_dir=args.cache,
16
+ )
17
+ if args.start is not None or args.end is not None:
18
+ resolved = resolved.select_rows(
19
+ args.start,
20
+ args.end,
21
+ )
22
+
23
+ if not args.checkpoint and not args.output:
24
+ raise ValueError("At least one of --checkpoint and --output should be set.")
25
+
26
+ if args.checkpoint:
27
+ output = args.checkpoint
28
+ out_dir = os.path.dirname(output)
29
+ base = os.path.basename(output)
30
+
31
+ resolved.save_checkpoint(output, save_source_data=args.force_write_data)
32
+
33
+ if args.output:
34
+ output = args.output
35
+ out_dir = os.path.dirname(output)
36
+ base = os.path.basename(output)
37
+ if len(out_dir) > 0:
38
+ os.makedirs(out_dir, exist_ok=True)
39
+ _save_dataset(
40
+ resolved.data,
41
+ output,
42
+ )
43
+ return None
@@ -0,0 +1,51 @@
1
+
2
+ from carabiner import print_err
3
+
4
+
5
+ def _resolve_and_slice_data(
6
+ data: str,
7
+ start: int | None = None,
8
+ end: int | None = None,
9
+ batch_size: int = 1024,
10
+ cache_dir: str = None
11
+ ):
12
+ from ..io import AutoDataset
13
+
14
+ ds = AutoDataset.load(data, cache_dir)
15
+ nrows = ds._dataset.num_rows
16
+ skip = 0 if start is None else start
17
+ take = (nrows if end is None else end) - skip
18
+ if (take - skip) < nrows:
19
+ print_err(f"[INFO] Reading dataset from row {skip} to row {take + skip} / {nrows}.")
20
+ return ds._dataset.skip(skip).take(take)
21
+
22
+
23
+ def _save_dataset(
24
+ dataset,
25
+ output: str
26
+ ) -> None:
27
+ print_err("[INFO] Saving dataset:\n" + str(dataset) + "\n" + f"at {output} as", end=" ")
28
+ if output.removesuffix(".gz").endswith((".csv", ".tsv", ".txt")):
29
+ print_err("CSV.")
30
+ dataset.to_csv(
31
+ output,
32
+ sep="," if output.removesuffix(".gz").endswith(".csv") else "\t",
33
+ compression='gzip' if output.endswith(".gz") else None,
34
+ )
35
+ elif output.endswith(".json"):
36
+ print_err("JSON.")
37
+ dataset.to_json(output)
38
+ elif output.endswith(".parquet"):
39
+ print_err("Parquet.")
40
+ dataset.to_parquet(output)
41
+ elif output.endswith(".sql"):
42
+ print_err("SQL.")
43
+ dataset.to_sql(output)
44
+ elif output.endswith(".hf"):
45
+ print_err("Hugging Face dataset.")
46
+ dataset.save_to_disk(output)
47
+ else:
48
+ print_err("Hugging Face dataset.")
49
+ dataset.save_to_disk(output + ".hf")
50
+ print_err(f"WARNING: Unsure what format to save as for filename {output}. Defaulted to Hugging Face dataset.")
51
+ return None
providence/data.py ADDED
@@ -0,0 +1,361 @@
1
+ """Data resolver class."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+ from collections.abc import Iterable, Mapping
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from carabiner import print_err
9
+
10
+ if TYPE_CHECKING:
11
+ from datasets import Dataset, DatasetDict, IterableDataset
12
+ from .collate import ColumnCollator
13
+ else:
14
+ Dataset, DatasetDict, IterableDataset = Any, Any, Any
15
+ ColumnCollator = Any
16
+
17
+ from . import app_name, __version__
18
+ from .io import AutoDataset, DataSource, autoload, file_checksum, save_json, load_json
19
+ from .package_data import CACHE_DIR
20
+ from .typing import DataLike
21
+
22
+
23
+ DEFAULT_FORMAT: str = "numpy"
24
+ CONFIG_FILENAME: str = "config.json"
25
+ DATA_FILENAME: str = "data.parquet"
26
+ EXAMPLE_FILENAME: str = "example.parquet"
27
+ SCHEMA_VERSION: str = "1"
28
+
29
+
30
+ class DataResolver:
31
+ """Resolve datasets and track their provenance.
32
+
33
+ Examples
34
+ --------
35
+ Select rows without modifying the original dataset:
36
+
37
+ >>> data = DataResolver({"x": list(range(10))})
38
+ >>> subset = data.select_rows(2, 5)
39
+ >>> list(subset.data["x"])
40
+ [2, 3, 4]
41
+ >>> len(data.data)
42
+ 10
43
+
44
+ Chain selections and restore a reference-only checkpoint:
45
+
46
+ >>> import tempfile
47
+ >>> from pathlib import Path
48
+ >>> import pandas as pd
49
+ >>> with tempfile.TemporaryDirectory() as tmp:
50
+ ... source = Path(tmp) / "data.parquet"
51
+ ... pd.DataFrame({"x": list(range(10))}).to_parquet(source)
52
+ ... selected = DataResolver(source).select_rows(2, 8).select_rows(1, 3)
53
+ ... selected.save(Path(tmp) / "checkpoint", save_source_data=False)
54
+ ... restored = DataResolver.load(Path(tmp) / "checkpoint")
55
+ ... print(list(restored.data["x"]))
56
+ [3, 4]
57
+
58
+ """
59
+ def __init__(
60
+ self,
61
+ data: str | Mapping[str, Any],
62
+ cache_dir: str | None = None,
63
+ _file_checksum: str | None = None,
64
+ _version: str = __version__,
65
+ _app: str = app_name
66
+ ):
67
+ self._version = _version
68
+ self._app = _app
69
+ self._data_requested = data
70
+ self.data = None
71
+ self._file_checksum = _file_checksum
72
+ self.selected_rows = None
73
+ self.data_source = None
74
+ self.data_example = None
75
+ self.cache_dir = cache_dir or CACHE_DIR
76
+ self._data_loaded = False
77
+ self._init()
78
+
79
+
80
+ def _init(self) -> None:
81
+ self.data = self._resolve_data(
82
+ self._data_requested,
83
+ )
84
+ if self.data is None:
85
+ raise AttributeError(
86
+ f"Data could not be loaded from {self._data_requested}."
87
+ )
88
+ self._data_loaded = self.data is not None
89
+
90
+ self.data_example, self.data_shape = self._inspect_data(
91
+ self.data,
92
+ self.data_example,
93
+ )
94
+ return None
95
+
96
+ def _resolve_data(
97
+ self,
98
+ data: DataLike,
99
+ cache_dir: str | None = None
100
+ ) -> Dataset | IterableDataset:
101
+ resolved = AutoDataset.load(
102
+ data,
103
+ cache_dir=cache_dir or self.cache_dir,
104
+ )
105
+ self.data_source = resolved.source
106
+ return resolved._dataset
107
+
108
+ def _apply_selection(
109
+ self,
110
+ start: int | None = None,
111
+ end: int | None = None
112
+ ) -> "DataResolver":
113
+ if self.selected_rows is None:
114
+ selected_rows = []
115
+ else:
116
+ selected_rows = self.selected_rows.copy()
117
+
118
+ nrows = self.data.num_rows
119
+ indices = tuple(slice(start, end).indices(nrows))
120
+ skip = indices[0]
121
+ take = indices[1] - skip
122
+
123
+ selected = type(self)(
124
+ self.data.select(range(*indices)),
125
+ cache_dir=self.cache_dir,
126
+ )
127
+
128
+ selected.data_source = self.data_source
129
+ selected_rows.append((start, end))
130
+ selected.selected_rows = selected_rows
131
+ return selected
132
+
133
+ @staticmethod
134
+ def _normalize_selected_rows(
135
+ start: Iterable[Iterable[int]] | int | None = None,
136
+ end: int | None = None
137
+ ):
138
+ if start is None and end is None:
139
+ return None
140
+ if isinstance(start, int) or (start is None and isinstance(end, int)):
141
+ return [(start, end)]
142
+ elif isinstance(start, Iterable) and not isinstance(start, str):
143
+ start = list(start)
144
+ if len(start) == 0:
145
+ raise ValueError(f"start was iterable but length-0: {start}")
146
+ elif len(start) == 2 and all(isinstance(item, int) for item in start):
147
+ start = [start]
148
+ for i, item in enumerate(start):
149
+ if isinstance(item, Iterable) and not isinstance(item, str):
150
+ if len(item) != 2:
151
+ raise ValueError(f"start was iterable but item {i} was not a 2-tuple: {item}")
152
+ else:
153
+ raise ValueError(f"start was iterable but item {i} was not: {item}\n{start=}")
154
+ return [tuple(item) for item in start]
155
+ else:
156
+ raise ValueError(f"Cannot normalize inputs: {start=}, {end=}")
157
+
158
+ def select_rows(
159
+ self,
160
+ start: Iterable[Iterable[int]] | int | None = None,
161
+ end: int | None = None
162
+ ) -> "DataResolver":
163
+ """Return a new resolver containing rows [start:end]."""
164
+
165
+ if start is None and end is None:
166
+ return self # pass through
167
+ selections = self._normalize_selected_rows(start, end)
168
+ data = self
169
+ for item in selections:
170
+ data = data._apply_selection(*item)
171
+ return data
172
+
173
+ @staticmethod
174
+ def _inspect_data(data: Dataset, example=None, format: str = DEFAULT_FORMAT) -> None:
175
+ if example is None:
176
+ if data is not None:
177
+ data = data.with_format(format)
178
+ example = data.take(1)
179
+ else:
180
+ raise AttributeError("Cannot inspect data without loading data or having an example")
181
+
182
+ first_item = example.with_format(format)[:1]
183
+ shape = {
184
+ col: first_item[col].shape[1:]
185
+ if not isinstance(first_item[col], dict)
186
+ else {
187
+ k: v.shape[1:] if v is not None else None
188
+ for k, v in first_item[col].items()
189
+ }
190
+ for col in example.column_names
191
+ }
192
+ return example, shape
193
+
194
+ def save(
195
+ self,
196
+ path: str | os.PathLike,
197
+ *,
198
+ save_source_data: bool | None = None,
199
+ discard_example_data: bool = False
200
+ ) -> None:
201
+ """Save the pipeline and required training-data artefacts.
202
+
203
+ Parameters
204
+ ==========
205
+ path
206
+ Checkpoint directory.
207
+ save_source_data
208
+ Whether to embed the input dataset.
209
+
210
+ ``None`` chooses automatically: immutable remote sources are
211
+ referenced, while local or ephemeral sources are packaged.
212
+
213
+ ``True`` always packages the resolved input data.
214
+
215
+ ``False`` never packages it.
216
+ discard_example_data
217
+ Normally, save one processed example when available. Set
218
+ discard_example_data=True to turn off.
219
+
220
+ """
221
+
222
+ if self.data_source is None:
223
+ source = DataSource()
224
+ else:
225
+ source = self.data_source
226
+
227
+
228
+ path = Path(path)
229
+ path.mkdir(parents=True, exist_ok=True)
230
+
231
+ if save_source_data is None:
232
+ save_source_data = not source.is_remote
233
+ if not save_source_data and source.uri is None:
234
+ raise ValueError(
235
+ "Reference-only checkpoint requires a source URI"
236
+ )
237
+ if save_source_data:
238
+ data_filename = str(path / DATA_FILENAME)
239
+ (
240
+ self.data
241
+ .with_format(None)
242
+ .to_parquet(data_filename)
243
+ )
244
+ checksum = file_checksum(data_filename)
245
+ else:
246
+ checksum = None
247
+
248
+ checkpoint_config = {
249
+ "source": source.to_config(),
250
+ "selected_rows": self.selected_rows,
251
+ "artefacts": {
252
+ "source_data": DATA_FILENAME if save_source_data else None,
253
+ "data_checksum": checksum,
254
+ "example": (
255
+ EXAMPLE_FILENAME
256
+ if (
257
+ not discard_example_data
258
+ and self.data_example is not None
259
+ )
260
+ else None
261
+ ),
262
+ },
263
+ "schema_version": SCHEMA_VERSION,
264
+ }
265
+
266
+ save_json(
267
+ checkpoint_config,
268
+ str(path / CONFIG_FILENAME),
269
+ )
270
+
271
+ if (
272
+ not discard_example_data
273
+ and self.data_example is not None
274
+ ):
275
+ (
276
+ self.data_example
277
+ .with_format(None)
278
+ .to_parquet(str(path / EXAMPLE_FILENAME))
279
+ )
280
+
281
+ @classmethod
282
+ def load(
283
+ cls,
284
+ path: str | os.PathLike,
285
+ *,
286
+ cache_dir: str | None = None,
287
+ ) -> "DataResolver":
288
+ """Restore a saved DataResolver and its available data artefacts."""
289
+ path = Path(path)
290
+ config = load_json(path / CONFIG_FILENAME)
291
+
292
+ data_source = DataSource.from_config(
293
+ config.get("source")
294
+ )
295
+ artefacts = config.get("artefacts", {})
296
+ data_filename = artefacts.get("source_data")
297
+ data_checksum = artefacts.get("data_checksum")
298
+ example_filename = artefacts.get("example")
299
+ selected_rows = config.get("selected_rows")
300
+ schema_version = config.get("schema_version")
301
+
302
+ if schema_version != SCHEMA_VERSION:
303
+ raise AttributeError(
304
+ f"Checkpoint schema version is {schema_version}, incompatible with package version {SCHEMA_VERSION}."
305
+ )
306
+
307
+ if data_filename is not None:
308
+ data_uri = str(path / data_filename)
309
+ this_checksum = file_checksum(data_uri)
310
+ if not this_checksum == data_checksum:
311
+ raise IOError(f"Checksum for {data_uri} should be {data_checksum}, but was {this_checksum}.")
312
+ has_embedded_data = True
313
+ elif data_source is not None:
314
+ if data_source.uri is None:
315
+ raise ValueError(
316
+ f"Reference-only checkpoint has no source URI:\n{data_source}"
317
+ )
318
+ elif (
319
+ not data_source.is_remote
320
+ and not os.path.exists(data_source.uri)
321
+ ):
322
+ raise FileNotFoundError(
323
+ "Checkpoint references non-remote data "
324
+ f"at {data_source.uri}, but that source does not exist."
325
+ )
326
+ if not data_source.is_remote:
327
+ data_source.assert_verified()
328
+ data_uri = data_source.uri
329
+ has_embedded_data = False
330
+ else:
331
+ raise AttributeError(f"Checkpoint is missing a data definition!\n{config=}")
332
+
333
+ data = cls(
334
+ data=data_uri,
335
+ cache_dir=cache_dir,
336
+ )
337
+ if not has_embedded_data and selected_rows is not None:
338
+ data = data.select_rows(selected_rows)
339
+ data.selected_rows = cls._normalize_selected_rows(selected_rows)
340
+ if data_source is not None:
341
+ data.data_source = data_source
342
+
343
+ if example_filename is not None:
344
+ data.data_example = autoload(
345
+ path / example_filename,
346
+ cache_dir=cache_dir,
347
+ )
348
+ if data.data_example is not None:
349
+ data.data_example, data.data_shape = data._inspect_data(
350
+ data.data,
351
+ data.data_example,
352
+ )
353
+ return data
354
+
355
+ # for compatibility
356
+ def save_checkpoint(self, *args, **kwargs):
357
+ return self.save(*args, **kwargs)
358
+
359
+ @classmethod
360
+ def load_checkpoint(cls, *args, **kwargs):
361
+ return cls.load(*args, **kwargs)