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 ADDED
@@ -0,0 +1,6 @@
1
+ """Utilities for preparing conversational datasets."""
2
+
3
+ from .splitting import split_jsonl
4
+
5
+ __version__ = "0.0.1"
6
+ __all__ = ["split_jsonl"]
smithtune/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ smithtune = smithtune.cli:main
@@ -0,0 +1 @@
1
+ smithtune