smithtune 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.
- smithtune/__init__.py +6 -0
- smithtune/__main__.py +3 -0
- smithtune/cli.py +32 -0
- smithtune/splitting.py +101 -0
- smithtune-0.0.1.dist-info/METADATA +62 -0
- smithtune-0.0.1.dist-info/RECORD +9 -0
- smithtune-0.0.1.dist-info/WHEEL +5 -0
- smithtune-0.0.1.dist-info/entry_points.txt +2 -0
- smithtune-0.0.1.dist-info/top_level.txt +1 -0
smithtune/__init__.py
ADDED
smithtune/__main__.py
ADDED
smithtune/cli.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Command-line interface for local dataset utilities."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, split_jsonl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv=None):
|
|
11
|
+
parser = argparse.ArgumentParser(description="Utilities for preparing conversational datasets.")
|
|
12
|
+
parser.add_argument("--version", action="version", version=f"smithtune {__version__}")
|
|
13
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
split = commands.add_parser("split", help="split a local JSONL file by group")
|
|
15
|
+
split.add_argument("input", help="input JSONL file")
|
|
16
|
+
split.add_argument("--group-by", required=True, help="top-level string or integer group field")
|
|
17
|
+
split.add_argument("--output-dir", required=True, help="new directory for the split files")
|
|
18
|
+
split.add_argument("--validation-fraction", type=float, default=0.1)
|
|
19
|
+
split.add_argument("--test-fraction", type=float, default=0.1)
|
|
20
|
+
split.add_argument("--seed", type=int, default=42)
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
try:
|
|
23
|
+
counts = split_jsonl(
|
|
24
|
+
args.input, args.output_dir, group_by=args.group_by,
|
|
25
|
+
validation_fraction=args.validation_fraction,
|
|
26
|
+
test_fraction=args.test_fraction, seed=args.seed,
|
|
27
|
+
)
|
|
28
|
+
except (ValueError, OSError) as error:
|
|
29
|
+
print(f"smithtune: {error}", file=sys.stderr)
|
|
30
|
+
return 2
|
|
31
|
+
print(json.dumps(counts))
|
|
32
|
+
return 0
|
smithtune/splitting.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Deterministic, grouped splitting of local JSONL files."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from contextlib import ExitStack
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SPLITS = ("train", "validation", "test")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _object(pairs):
|
|
13
|
+
result = {}
|
|
14
|
+
for key, value in pairs:
|
|
15
|
+
if key in result:
|
|
16
|
+
raise ValueError("duplicate JSON key")
|
|
17
|
+
result[key] = value
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reject_constant(value):
|
|
22
|
+
raise ValueError("nonstandard JSON constant")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def split_jsonl(
|
|
26
|
+
input_path: str | Path,
|
|
27
|
+
output_dir: str | Path,
|
|
28
|
+
*,
|
|
29
|
+
group_by: str,
|
|
30
|
+
validation_fraction: float = 0.1,
|
|
31
|
+
test_fraction: float = 0.1,
|
|
32
|
+
seed: int = 42,
|
|
33
|
+
) -> dict[str, int]:
|
|
34
|
+
"""Split JSON objects by a top-level string or integer group ID.
|
|
35
|
+
|
|
36
|
+
Return record counts per split. Validate all input before creating output.
|
|
37
|
+
Group assignments remain stable with unchanged IDs, seed and fractions.
|
|
38
|
+
The complete input is held in memory. Existing output paths are rejected.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(group_by, str) or not group_by:
|
|
41
|
+
raise ValueError("group_by must be a nonempty field name")
|
|
42
|
+
if type(seed) is not int:
|
|
43
|
+
raise ValueError("seed must be an integer")
|
|
44
|
+
for fraction in (validation_fraction, test_fraction):
|
|
45
|
+
if (isinstance(fraction, bool) or not isinstance(fraction, (int, float))
|
|
46
|
+
or not math.isfinite(fraction) or not 0 <= fraction <= 1):
|
|
47
|
+
raise ValueError("fractions must be finite numbers between 0 and 1")
|
|
48
|
+
if validation_fraction + test_fraction > 1:
|
|
49
|
+
raise ValueError("validation and test fractions cannot total more than 1")
|
|
50
|
+
|
|
51
|
+
destination = Path(output_dir)
|
|
52
|
+
if destination.exists() or destination.is_symlink():
|
|
53
|
+
raise FileExistsError(f"output directory already exists: {destination}")
|
|
54
|
+
|
|
55
|
+
partitions: dict[str, list[bytes]] = {name: [] for name in SPLITS}
|
|
56
|
+
validation_cutoff = int(validation_fraction * 2**256)
|
|
57
|
+
test_cutoff = int((validation_fraction + test_fraction) * 2**256)
|
|
58
|
+
with Path(input_path).open("rb") as source:
|
|
59
|
+
for line_number, raw in enumerate(source, 1):
|
|
60
|
+
try:
|
|
61
|
+
record = json.loads(
|
|
62
|
+
raw.decode("utf-8"), object_pairs_hook=_object,
|
|
63
|
+
parse_constant=_reject_constant,
|
|
64
|
+
)
|
|
65
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
66
|
+
raise ValueError(f"line {line_number}: invalid JSON object") from None
|
|
67
|
+
if not isinstance(record, dict):
|
|
68
|
+
raise ValueError(f"line {line_number}: expected a JSON object")
|
|
69
|
+
group = record.get(group_by)
|
|
70
|
+
if not (type(group) is int or isinstance(group, str) and bool(group.strip())):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"line {line_number}: group field must contain a nonempty string or integer"
|
|
73
|
+
)
|
|
74
|
+
identity = json.dumps([seed, group], ensure_ascii=True, separators=(",", ":"))
|
|
75
|
+
rank = int.from_bytes(hashlib.sha256(identity.encode("ascii")).digest(), "big")
|
|
76
|
+
split = "validation" if rank < validation_cutoff else (
|
|
77
|
+
"test" if rank < test_cutoff else "train"
|
|
78
|
+
)
|
|
79
|
+
partitions[split].append(raw if raw.endswith(b"\n") else raw + b"\n")
|
|
80
|
+
|
|
81
|
+
# mkdir is exclusive even if another process created the path during validation.
|
|
82
|
+
destination.mkdir()
|
|
83
|
+
created = []
|
|
84
|
+
try:
|
|
85
|
+
with ExitStack() as stack:
|
|
86
|
+
handles = {}
|
|
87
|
+
for name in SPLITS:
|
|
88
|
+
path = destination / f"{name}.jsonl"
|
|
89
|
+
handles[name] = stack.enter_context(path.open("xb"))
|
|
90
|
+
created.append(path)
|
|
91
|
+
for name, records in partitions.items():
|
|
92
|
+
handles[name].writelines(records)
|
|
93
|
+
except BaseException:
|
|
94
|
+
for path in created:
|
|
95
|
+
path.unlink(missing_ok=True)
|
|
96
|
+
try:
|
|
97
|
+
destination.rmdir()
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
raise
|
|
101
|
+
return {name: len(records) for name, records in partitions.items()}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smithtune
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Utilities for preparing conversational datasets.
|
|
5
|
+
Classifier: Development Status :: 3 - Alpha
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Topic :: Utilities
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# smithtune
|
|
12
|
+
|
|
13
|
+
Utilities for preparing conversational datasets. Runs locally with no runtime dependencies.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10 or newer.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install smithtune
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Split a dataset
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
smithtune split data.jsonl --group-by conversation_id --output-dir splits/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each input line must be a JSON object with a top-level group field containing a nonempty string or integer:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{"conversation_id":"a","messages":[{"role":"user","content":"Hello"}]}
|
|
33
|
+
{"conversation_id":"a","messages":[{"role":"assistant","content":"Hi"}]}
|
|
34
|
+
{"conversation_id":"b","messages":[{"role":"user","content":"Good morning"}]}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Creates `train.jsonl`, `validation.jsonl`, and `test.jsonl` and prints record counts. Records with the same group ID always go into the same file. Use IDs that are unique across your input sources.
|
|
38
|
+
|
|
39
|
+
Defaults target 80% training, 10% validation, and 10% test **by group**. Hash-based assignment gives approximate proportions; small datasets may have empty splits. Adding or reordering records does not change a group's assignment when its ID, seed, and fractions stay the same.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
smithtune split data.jsonl --group-by conversation_id --output-dir splits-v2/ \
|
|
43
|
+
--validation-fraction 0.15 --test-fraction 0.15 --seed 42
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The splitter preserves record bytes and order within each output file, adding a final newline if needed. It validates the entire input in memory before writing. Blank lines, invalid JSON, duplicate JSON keys, and missing or invalid group IDs are rejected with a line number. String IDs and integer IDs are distinct.
|
|
47
|
+
|
|
48
|
+
The output directory must not exist, and its parent directory must exist. Existing files are never overwritten. No data is uploaded or fetched.
|
|
49
|
+
|
|
50
|
+
## Python
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from smithtune import split_jsonl
|
|
54
|
+
|
|
55
|
+
counts = split_jsonl("data.jsonl", "splits", group_by="conversation_id")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
62
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
smithtune/__init__.py,sha256=TNpaLUmqJv-Bv1IrhMbG0-J0XJh108j4d2gJGiAGZhc,140
|
|
2
|
+
smithtune/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
smithtune/cli.py,sha256=3zmFFZJajGdipGBcrZre2Fz-V7_Vl5ZIn2ZMZyHoBsU,1378
|
|
4
|
+
smithtune/splitting.py,sha256=Dq_zKWwqIpf1QUoAlwqeZGFlMkf0laauFPIj9aPcaD0,4012
|
|
5
|
+
smithtune-0.0.1.dist-info/METADATA,sha256=JU1OwzDSgams2iRSb5nn-y373Ctd2xIwFSt-cM0nUbk,2246
|
|
6
|
+
smithtune-0.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
smithtune-0.0.1.dist-info/entry_points.txt,sha256=27zgxv1bwqf-PkIt6Hs55I41huEbTtGVLhFLgWvhUes,49
|
|
8
|
+
smithtune-0.0.1.dist-info/top_level.txt,sha256=b5wBjEVK75Pskj4x9_S_066AWSxXtSKkwnQfEtS0m7Q,10
|
|
9
|
+
smithtune-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smithtune
|