itar 0.2.0__tar.gz → 0.3.0__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.
- {itar-0.2.0 → itar-0.3.0}/PKG-INFO +10 -5
- {itar-0.2.0 → itar-0.3.0}/README.md +9 -4
- {itar-0.2.0 → itar-0.3.0}/pyproject.toml +1 -1
- {itar-0.2.0 → itar-0.3.0}/src/itar/cli.py +80 -31
- {itar-0.2.0 → itar-0.3.0}/src/itar/__init__.py +0 -0
- {itar-0.2.0 → itar-0.3.0}/src/itar/index.py +0 -0
- {itar-0.2.0 → itar-0.3.0}/src/itar/indexed_tar_file.py +0 -0
- {itar-0.2.0 → itar-0.3.0}/src/itar/py.typed +0 -0
- {itar-0.2.0 → itar-0.3.0}/src/itar/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: itar
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: Karim Abou Zeid
|
|
6
6
|
Author-email: Karim Abou Zeid <contact@ka.codes>
|
|
@@ -13,14 +13,18 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
|
|
14
14
|
# itar
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
[](https://pypi.python.org/pypi/itar)
|
|
17
|
+
|
|
18
|
+
`itar` builds constant‑time indexes over one or more tar file shards, enabling direct, random access to members without extracting the archives. It ships a lightweight CLI (`itar`) and a Python API.
|
|
19
|
+
|
|
20
|
+
Designed for large datasets and deep‑learning pipelines, it supports single or sharded tar archives with thread‑safe access for concurrent reads.
|
|
17
21
|
|
|
18
22
|
## Quickstart (single tarball)
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
echo "Hello world!" > hello.txt
|
|
22
26
|
tar cf hello.tar hello.txt # regular tarball
|
|
23
|
-
itar index
|
|
27
|
+
itar index create hello.itar # indexes hello.tar
|
|
24
28
|
itar index list hello.itar # list indexed members
|
|
25
29
|
```
|
|
26
30
|
|
|
@@ -43,7 +47,7 @@ Give each shard a zero-padded suffix before building the index:
|
|
|
43
47
|
```bash
|
|
44
48
|
tar cf photos-0.tar wedding/ # shard 0
|
|
45
49
|
tar cf photos-1.tar vacation/ # shard 1
|
|
46
|
-
itar index
|
|
50
|
+
itar index create photos.itar # discovers photos-0.tar, photos-1.tar, ...
|
|
47
51
|
itar index list -l photos.itar # shard index, offsets, byte sizes
|
|
48
52
|
```
|
|
49
53
|
|
|
@@ -64,9 +68,10 @@ num_shards, stored_index = itar.index.load("photos.itar")
|
|
|
64
68
|
|
|
65
69
|
| Command | Purpose |
|
|
66
70
|
| --- | --- |
|
|
67
|
-
| `itar index
|
|
71
|
+
| `itar index create <archive>.itar [--single TAR \| --shards shard0.tar shard1.tar ...]` | Indexes a single archive or an explicit set of shards. With no flags, shards are auto-discovered next to `<archive>.itar`. |
|
|
68
72
|
| `itar index list <archive>.itar` | Lists members. Use `-l` for shard/offset info and `-H` for human-readable sizes. |
|
|
69
73
|
| `itar index check <archive>.itar` | Validates recorded entries; add `--member NAME` to focus on specific files. |
|
|
74
|
+
| `itar cat <archive>.itar <member>` | Streams a member’s bytes to stdout. |
|
|
70
75
|
|
|
71
76
|
## Python helpers
|
|
72
77
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# itar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://pypi.python.org/pypi/itar)
|
|
4
|
+
|
|
5
|
+
`itar` builds constant‑time indexes over one or more tar file shards, enabling direct, random access to members without extracting the archives. It ships a lightweight CLI (`itar`) and a Python API.
|
|
6
|
+
|
|
7
|
+
Designed for large datasets and deep‑learning pipelines, it supports single or sharded tar archives with thread‑safe access for concurrent reads.
|
|
4
8
|
|
|
5
9
|
## Quickstart (single tarball)
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
echo "Hello world!" > hello.txt
|
|
9
13
|
tar cf hello.tar hello.txt # regular tarball
|
|
10
|
-
itar index
|
|
14
|
+
itar index create hello.itar # indexes hello.tar
|
|
11
15
|
itar index list hello.itar # list indexed members
|
|
12
16
|
```
|
|
13
17
|
|
|
@@ -30,7 +34,7 @@ Give each shard a zero-padded suffix before building the index:
|
|
|
30
34
|
```bash
|
|
31
35
|
tar cf photos-0.tar wedding/ # shard 0
|
|
32
36
|
tar cf photos-1.tar vacation/ # shard 1
|
|
33
|
-
itar index
|
|
37
|
+
itar index create photos.itar # discovers photos-0.tar, photos-1.tar, ...
|
|
34
38
|
itar index list -l photos.itar # shard index, offsets, byte sizes
|
|
35
39
|
```
|
|
36
40
|
|
|
@@ -51,9 +55,10 @@ num_shards, stored_index = itar.index.load("photos.itar")
|
|
|
51
55
|
|
|
52
56
|
| Command | Purpose |
|
|
53
57
|
| --- | --- |
|
|
54
|
-
| `itar index
|
|
58
|
+
| `itar index create <archive>.itar [--single TAR \| --shards shard0.tar shard1.tar ...]` | Indexes a single archive or an explicit set of shards. With no flags, shards are auto-discovered next to `<archive>.itar`. |
|
|
55
59
|
| `itar index list <archive>.itar` | Lists members. Use `-l` for shard/offset info and `-H` for human-readable sizes. |
|
|
56
60
|
| `itar index check <archive>.itar` | Validates recorded entries; add `--member NAME` to focus on specific files. |
|
|
61
|
+
| `itar cat <archive>.itar <member>` | Streams a member’s bytes to stdout. |
|
|
57
62
|
|
|
58
63
|
## Python helpers
|
|
59
64
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import re
|
|
3
2
|
import sys
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
@@ -12,15 +11,36 @@ def main() -> None:
|
|
|
12
11
|
subparsers = parser.add_subparsers(dest="command")
|
|
13
12
|
subparsers.required = True
|
|
14
13
|
|
|
14
|
+
cat_parser = subparsers.add_parser(
|
|
15
|
+
"cat", help="Stream a member from an index to stdout"
|
|
16
|
+
)
|
|
17
|
+
cat_parser.add_argument("index", type=Path, help="Path to the .itar index file")
|
|
18
|
+
cat_parser.add_argument("member", help="Member path to stream")
|
|
19
|
+
cat_parser.set_defaults(func=_cmd_cat)
|
|
20
|
+
|
|
21
|
+
ls_parser = subparsers.add_parser(
|
|
22
|
+
"ls", aliases=["list"], help="List members recorded in an index"
|
|
23
|
+
)
|
|
24
|
+
ls_parser.add_argument("index", type=Path, help="Path to the .itar index file")
|
|
25
|
+
ls_parser.add_argument(
|
|
26
|
+
"-l", "--long", action="store_true", help="Show shard and offset details"
|
|
27
|
+
)
|
|
28
|
+
ls_parser.add_argument(
|
|
29
|
+
"--bytes",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Render sizes as number of bytes",
|
|
32
|
+
)
|
|
33
|
+
ls_parser.set_defaults(func=_cmd_ls)
|
|
34
|
+
|
|
15
35
|
index_parser = subparsers.add_parser("index", help="Manage .itar index files")
|
|
16
36
|
index_subparsers = index_parser.add_subparsers(dest="index_command")
|
|
17
37
|
index_subparsers.required = True
|
|
18
38
|
|
|
19
|
-
|
|
20
|
-
"
|
|
39
|
+
create_parser = index_subparsers.add_parser(
|
|
40
|
+
"create", help="Create an index file from one or more TAR shards"
|
|
21
41
|
)
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
create_parser.add_argument("index", type=Path, help="Destination .itar index file")
|
|
43
|
+
create_parser.add_argument(
|
|
24
44
|
"--shards",
|
|
25
45
|
type=Path,
|
|
26
46
|
nargs="+",
|
|
@@ -29,20 +49,20 @@ def main() -> None:
|
|
|
29
49
|
"Explicit shard files to index. Use when you want to bypass auto-discovery."
|
|
30
50
|
),
|
|
31
51
|
)
|
|
32
|
-
|
|
52
|
+
create_parser.add_argument(
|
|
33
53
|
"--single",
|
|
34
54
|
type=Path,
|
|
35
55
|
dest="single_tar",
|
|
36
56
|
metavar="TAR",
|
|
37
57
|
help="Path to a single TAR archive. Ignored if --shards is provided.",
|
|
38
58
|
)
|
|
39
|
-
|
|
59
|
+
create_parser.add_argument(
|
|
40
60
|
"--no-progress",
|
|
41
61
|
dest="progress",
|
|
42
62
|
action="store_false",
|
|
43
63
|
help="Disable the indexing progress bar",
|
|
44
64
|
)
|
|
45
|
-
|
|
65
|
+
create_parser.set_defaults(progress=True, func=_cmd_index_create)
|
|
46
66
|
|
|
47
67
|
check_parser = index_subparsers.add_parser(
|
|
48
68
|
"check", help="Verify every member recorded in an index file"
|
|
@@ -64,10 +84,9 @@ def main() -> None:
|
|
|
64
84
|
"-l", "--long", action="store_true", help="Show shard and offset details"
|
|
65
85
|
)
|
|
66
86
|
list_parser.add_argument(
|
|
67
|
-
"
|
|
68
|
-
"--human-readable",
|
|
87
|
+
"--bytes",
|
|
69
88
|
action="store_true",
|
|
70
|
-
help="Render sizes
|
|
89
|
+
help="Render sizes as number of bytes",
|
|
71
90
|
)
|
|
72
91
|
list_parser.set_defaults(func=_cmd_index_list)
|
|
73
92
|
|
|
@@ -83,7 +102,7 @@ class CLIError(RuntimeError):
|
|
|
83
102
|
"""Raised for recoverable CLI errors."""
|
|
84
103
|
|
|
85
104
|
|
|
86
|
-
def
|
|
105
|
+
def _resolve_shards_for_create(
|
|
87
106
|
index_path: Path,
|
|
88
107
|
explicit_shards: list[Path] | None,
|
|
89
108
|
single_tar: Path | None,
|
|
@@ -98,15 +117,8 @@ def _resolve_shards_for_build(
|
|
|
98
117
|
raise CLIError(f"Single tar not found: {single_tar}")
|
|
99
118
|
return single_tar, None
|
|
100
119
|
|
|
101
|
-
stem = index_path.stem
|
|
102
|
-
pattern = re.compile(rf"^{re.escape(stem)}-\d+\.tar$")
|
|
103
|
-
indexed_shards = [
|
|
104
|
-
candidate
|
|
105
|
-
for candidate in index_path.parent.glob(f"{index_path.stem}-*.tar")
|
|
106
|
-
if candidate.is_file() and pattern.match(candidate.name)
|
|
107
|
-
]
|
|
108
|
-
indexed_shards.sort()
|
|
109
120
|
layout = index.IndexLayout(index_path)
|
|
121
|
+
indexed_shards = layout.discover_shards()
|
|
110
122
|
single_tar_candidate = layout.single_tar()
|
|
111
123
|
has_single = single_tar_candidate.is_file()
|
|
112
124
|
has_indexed = len(indexed_shards) > 0
|
|
@@ -139,9 +151,30 @@ def _resolve_shards_for_build(
|
|
|
139
151
|
return shards, num_shards
|
|
140
152
|
|
|
141
153
|
|
|
142
|
-
def
|
|
154
|
+
def _cmd_cat(args) -> None:
|
|
155
|
+
try:
|
|
156
|
+
with index.open(args.index) as archive:
|
|
157
|
+
try:
|
|
158
|
+
source = archive.file(args.member)
|
|
159
|
+
except KeyError as exc: # pragma: no cover - defensive
|
|
160
|
+
raise CLIError(f"Member not found: {args.member}") from exc
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
while True:
|
|
164
|
+
chunk = source.read(64 * 1024)
|
|
165
|
+
if not chunk:
|
|
166
|
+
break
|
|
167
|
+
sys.stdout.buffer.write(chunk)
|
|
168
|
+
sys.stdout.buffer.flush()
|
|
169
|
+
finally:
|
|
170
|
+
source.close()
|
|
171
|
+
except FileNotFoundError as exc: # pragma: no cover - defensive
|
|
172
|
+
raise CLIError(str(exc)) from exc
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _cmd_index_create(args) -> None:
|
|
143
176
|
index_path = Path(args.index)
|
|
144
|
-
shards, num_shards =
|
|
177
|
+
shards, num_shards = _resolve_shards_for_create(
|
|
145
178
|
index_path, args.shards, args.single_tar
|
|
146
179
|
)
|
|
147
180
|
index.create(index_path, shards, progress_bar=args.progress)
|
|
@@ -169,11 +202,32 @@ def _cmd_index_check(args) -> None:
|
|
|
169
202
|
sys.exit(1)
|
|
170
203
|
|
|
171
204
|
|
|
205
|
+
def _cmd_ls(args) -> None:
|
|
206
|
+
_, current_index = index.load(args.index)
|
|
207
|
+
if args.long:
|
|
208
|
+
max_size = 0
|
|
209
|
+
lines = []
|
|
210
|
+
for member, (shard_idx, (offset, offset_data, size)) in current_index.items():
|
|
211
|
+
if not args.bytes:
|
|
212
|
+
from humanize import naturalsize
|
|
213
|
+
|
|
214
|
+
size = naturalsize(size, gnu=True)
|
|
215
|
+
else:
|
|
216
|
+
size = str(size)
|
|
217
|
+
max_size = max(max_size, len(size))
|
|
218
|
+
lines.append((size, member))
|
|
219
|
+
for line in [f"{size:>{max_size}} {member}" for size, member in lines]:
|
|
220
|
+
print(line)
|
|
221
|
+
else:
|
|
222
|
+
for member in current_index:
|
|
223
|
+
print(member)
|
|
224
|
+
|
|
225
|
+
|
|
172
226
|
def _cmd_index_list(args) -> None:
|
|
173
227
|
_, current_index = index.load(args.index)
|
|
174
228
|
if args.long:
|
|
175
229
|
for member, (shard_idx, (offset, offset_data, size)) in current_index.items():
|
|
176
|
-
if args.
|
|
230
|
+
if not args.bytes:
|
|
177
231
|
from humanize import naturalsize
|
|
178
232
|
|
|
179
233
|
size = naturalsize(size, gnu=True)
|
|
@@ -181,11 +235,6 @@ def _cmd_index_list(args) -> None:
|
|
|
181
235
|
f"{member:<40} {shard_idx:>5} {offset:>12} {offset_data:>12} {size:>10}"
|
|
182
236
|
)
|
|
183
237
|
print(f"{'NAME':<40} {'SHARD':>5} {'OFFSET':>12} {'OFF_DATA':>12} {'SIZE':>10}")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
print(member)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if __name__ == "__main__":
|
|
191
|
-
main()
|
|
238
|
+
else:
|
|
239
|
+
for member in current_index:
|
|
240
|
+
print(member)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|