aspect-data 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.
aspect/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from importlib.metadata import version
2
+
3
+ app_name = "aspect-data"
4
+ __author__ = "Eachan Johnson"
5
+ __version__ = version(app_name)
@@ -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,129 @@
1
+ """Command-line interface for aspect."""
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 .featurize import _featurize, _serialize
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
+ feature_cols = CLIOption(
27
+ '--features', '-x',
28
+ type=str,
29
+ nargs='*',
30
+ default=None,
31
+ help='Featurization spec: column_name[:transform[(kwargs)]:...][@output_name]...',
32
+ )
33
+ cache = CLIOption(
34
+ '--cache',
35
+ type=str,
36
+ default=None,
37
+ help='Where to cache data.',
38
+ )
39
+ _config = CLIOption(
40
+ '--config',
41
+ type=str,
42
+ default=None,
43
+ help='Load pipeline from this config or checkpoint. Default: do not use, process from scratch.',
44
+ )
45
+ _checkpoint = CLIOption(
46
+ '--checkpoint',
47
+ type=str,
48
+ default=None,
49
+ help='Save data at this checkpoint. Default: do not save checkpoint.',
50
+ )
51
+
52
+ output_name = CLIOption(
53
+ '--output', '-o',
54
+ type=str,
55
+ required=True,
56
+ help='Output filename.',
57
+ )
58
+
59
+ # slice dataset
60
+ slice_start = CLIOption(
61
+ '--start',
62
+ type=int,
63
+ default=0,
64
+ help='First row of dataset to process.',
65
+ )
66
+ slice_end = CLIOption(
67
+ '--end',
68
+ type=int,
69
+ default=None,
70
+ help='Last row of dataset to process. Default: end of dataset.',
71
+ )
72
+ extra_cols = CLIOption(
73
+ '--extras',
74
+ type=str,
75
+ nargs="*",
76
+ default=None,
77
+ help='Extra columns to retain without transformation.',
78
+ )
79
+ random_seed = CLIOption(
80
+ '--seed', '-e',
81
+ type=int,
82
+ default=None,
83
+ help='Random seed. Default: determininstic.',
84
+ )
85
+
86
+ serialize = CLICommand(
87
+ "serialize",
88
+ description="Checkpoint a feature spec.",
89
+ options=[
90
+ output_name,
91
+ feature_cols,
92
+ extra_cols,
93
+ ],
94
+ main=_serialize,
95
+ )
96
+
97
+ featurize = CLICommand(
98
+ "featurize",
99
+ description="Featurize a table.",
100
+ options=[
101
+ input_filename,
102
+ output_name,
103
+ feature_cols,
104
+ extra_cols,
105
+ slice_start,
106
+ slice_end,
107
+ _config,
108
+ _checkpoint,
109
+ random_seed,
110
+ cache,
111
+ ],
112
+ main=_featurize,
113
+ )
114
+
115
+ app = CLIApp(
116
+ app_name,
117
+ description="Serializable featurization pipelines for ML/AI on chemistry, taxonomy, and general tabular data.",
118
+ version=__version__,
119
+ commands=[
120
+ serialize,
121
+ featurize,
122
+ ],
123
+ )
124
+ app.run()
125
+ return None
126
+
127
+
128
+ if __name__ == '__main__':
129
+ main()
@@ -0,0 +1,187 @@
1
+ from typing import Optional, Iterable, Union
2
+
3
+ from argparse import Namespace
4
+ import os
5
+
6
+ from carabiner import print_err
7
+ from carabiner.cliutils import clicommand
8
+
9
+ from .io import _resolve_and_slice_data, _save_dataset
10
+
11
+
12
+ def _parse_feature_spec(x: str, i: int):
13
+ if ":" in x:
14
+ input_col, _, remainder = x.partition(":")
15
+ transforms, _, output_col = remainder.rpartition("@")
16
+ else:
17
+ input_col, _, output_col = x.partition("@")
18
+ transforms, remainder = "", ""
19
+ # print(x, " | ".join([input_col, remainder, transforms, output_col]))
20
+ if not output_col:
21
+ if not transforms:
22
+ output_col = input_col
23
+ extra = True
24
+ else:
25
+ output_col = f"col_{i:03d}"
26
+ else:
27
+ extra = False
28
+ transforms = transforms.split(":")
29
+ if transforms == [""]:
30
+ return extra, {output_col: (input_col, {"name": "identity"})}
31
+ parsed_transforms = []
32
+ for transform in transforms:
33
+ name, _, kwargs = transform.partition("(")
34
+ if not kwargs:
35
+ parsed_transforms.append({"name": name})
36
+ continue
37
+ kwargs = kwargs.removesuffix(")").split(",")
38
+ key_vals = [k.strip().split("=") for k in kwargs]
39
+ wrong_number = [k for k in key_vals if len(k) != 2]
40
+ if wrong_number:
41
+ raise ValueError(f"Badly formatted kwargs: {kwargs=}, parsed to {key_vals}")
42
+ kwargs = {
43
+ key.strip(): value.strip()
44
+ for key, value in key_vals
45
+ }
46
+ parsed_kwargs = {}
47
+ for k, v in kwargs.items():
48
+ if v.isdigit():
49
+ parsed_v = int(v)
50
+ elif v.casefold() == "true":
51
+ parsed_v = True
52
+ elif v.casefold() == "false":
53
+ parsed_v = False
54
+ else:
55
+ try:
56
+ parsed_v = float(v)
57
+ except:
58
+ parsed_v = v
59
+ parsed_kwargs[k] = parsed_v
60
+ parsed_transforms.append({
61
+ "name": name,
62
+ } | parsed_kwargs)
63
+ return extra, {output_col: (input_col, tuple(parsed_transforms))}
64
+
65
+
66
+ def parse_feature_specs(x: Union[str, Iterable[str]]):
67
+ if isinstance(x, str):
68
+ x = [x]
69
+ column_transforms = []
70
+ extra_cols = []
71
+ for i, _x in enumerate(x):
72
+ extra, column_transform = _parse_feature_spec(_x, i)
73
+ if extra:
74
+ extra_cols.append(_x)
75
+ else:
76
+ column_transforms.append(column_transform)
77
+ return extra_cols, {k: v for d in column_transforms for k, v in d.items()}
78
+
79
+
80
+ def _common_feature_spec_routine(x: Union[str, Iterable[str]], args_extras=None):
81
+ extras, column_transforms = parse_feature_specs(x)
82
+ extras += (args_extras or [])
83
+ extras = sorted(set(extras))
84
+
85
+ print_err(
86
+ f"""
87
+ Parsed the feature spec:
88
+ - Column transforms: {column_transforms}
89
+ - Other columns to retain: {extras}
90
+
91
+ """
92
+ )
93
+ return extras, column_transforms
94
+
95
+
96
+ def _validate_checkpoints(chk1, path):
97
+ from ..data import DataPipeline
98
+ print_err(f"[INFO] Validating checkpoint at {path}...", end="")
99
+ pipeline2 = DataPipeline().load_checkpoint(path)
100
+ if chk1 != pipeline2:
101
+ attr1, attr2 = vars(chk1), vars(pipeline2)
102
+ wrong_attributes = {
103
+ k: (attr1[k], "!=", v)
104
+ for k, v in attr2.items()
105
+ if v != attr1[k]
106
+ }
107
+ raise IOError(
108
+ f"Checkpoint at {path} does not recreate the same object. "
109
+ f"These attributes were different: {wrong_attributes}"
110
+ )
111
+ print_err(" all good!")
112
+ return None
113
+
114
+
115
+ @clicommand("Serializing featurization with the following parameters")
116
+ def _serialize(args: Namespace) -> None:
117
+
118
+ from ..data import DataPipeline
119
+
120
+ output = args.output
121
+ out_dir = os.path.dirname(output)
122
+ base = os.path.basename(output)
123
+ if len(out_dir) > 0:
124
+ os.makedirs(out_dir, exist_ok=True)
125
+
126
+ extras, column_transforms = _common_feature_spec_routine(
127
+ args.features,
128
+ args.extras,
129
+ )
130
+ pipeline = DataPipeline(
131
+ column_transforms=column_transforms,
132
+ columns_to_keep=extras,
133
+ )
134
+ pipeline.save_checkpoint(output)
135
+ _validate_checkpoints(pipeline, output)
136
+ return None
137
+
138
+
139
+ @clicommand("Featurizing data with the following parameters")
140
+ def _featurize(args: Namespace) -> None:
141
+
142
+ from ..data import DataPipeline
143
+
144
+ if args.features:
145
+ extras, column_transforms = _common_feature_spec_routine(args.features)
146
+ pipeline = DataPipeline(
147
+ column_transforms=column_transforms,
148
+ columns_to_keep=extras,
149
+ cache_dir=args.cache,
150
+ )
151
+ elif args.config:
152
+ extras = args.extras or []
153
+ pipeline = DataPipeline(
154
+ cache_dir=args.cache,
155
+ ).load_checkpoint(args.config)
156
+ else:
157
+ raise ValueError(f"One of --features or --config must be provided.")
158
+
159
+ ds = _resolve_and_slice_data(
160
+ args.input_file,
161
+ start=args.start,
162
+ end=args.end,
163
+ cache_dir=args.cache,
164
+ )
165
+ ds = pipeline(
166
+ ds,
167
+ keep_extra_columns=extras,
168
+ drop_unused_columns=True,
169
+ )
170
+ if args.checkpoint:
171
+ output = args.checkpoint
172
+ out_dir = os.path.dirname(output)
173
+ base = os.path.basename(output)
174
+ pipeline.save_checkpoint(output)
175
+ _validate_checkpoints(pipeline, output)
176
+
177
+ if args.output:
178
+ output = args.output
179
+ out_dir = os.path.dirname(output)
180
+ base = os.path.basename(output)
181
+ if len(out_dir) > 0:
182
+ os.makedirs(out_dir, exist_ok=True)
183
+ _save_dataset(
184
+ ds,
185
+ output,
186
+ )
187
+ return None
@@ -0,0 +1,52 @@
1
+ from typing import Mapping, Optional, Union
2
+
3
+ from carabiner import print_err
4
+
5
+
6
+ def _resolve_and_slice_data(
7
+ data: str,
8
+ start: Optional[int] = None,
9
+ end: Optional[int] = None,
10
+ batch_size: int = 1024,
11
+ cache_dir: str = None
12
+ ):
13
+ from ..io import AutoDataset
14
+
15
+ candidates_ds = AutoDataset.load(data, cache_dir)._dataset
16
+ nrows = candidates_ds.num_rows
17
+ skip = start or 0
18
+ take = (end or nrows) - skip
19
+ if (take - skip) < nrows:
20
+ print_err(f"[INFO] Reading dataset from row {skip} to row {take + skip} / {nrows}.")
21
+ return candidates_ds.skip(skip).take(take)
22
+
23
+
24
+ def _save_dataset(
25
+ dataset,
26
+ output: str
27
+ ) -> None:
28
+ print_err("[INFO] Saving dataset:\n" + str(dataset) + "\n" + f"at {output} as", end=" ")
29
+ if output.removesuffix(".gz").endswith((".csv", ".tsv", ".txt")):
30
+ print_err("CSV.")
31
+ dataset.to_csv(
32
+ output,
33
+ sep="," if output.removesuffix(".gz").endswith(".csv") else "\t",
34
+ compression='gzip' if output.endswith(".gz") else None,
35
+ )
36
+ elif output.endswith(".json"):
37
+ print_err("JSON.")
38
+ dataset.to_json(output)
39
+ elif output.endswith(".parquet"):
40
+ print_err("Parquet.")
41
+ dataset.to_parquet(output)
42
+ elif output.endswith(".sql"):
43
+ print_err("SQL.")
44
+ dataset.to_sql(output)
45
+ elif output.endswith(".hf"):
46
+ print_err("Hugging Face dataset.")
47
+ dataset.save_to_disk(output)
48
+ else:
49
+ print_err("Hugging Face dataset.")
50
+ dataset.save_to_disk(output + ".hf")
51
+ print_err(f"WARNING: Unsure what format to save as for filename {output}. Defaulted to Hugging Face dataset.")
52
+ return None