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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: itar
3
- Version: 0.2.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
- `itar` builds constant-time indexes for one or more TAR shards so you can seek directly to a member without extracting the archive. The project ships a small CLI (`itar`) and a Python helper (`IndexedTarFile`) plus a module-level `itar.open()` convenience that mirrors `tarfile.open()`.
16
+ [![image](https://img.shields.io/pypi/v/itar.svg)](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 build hello.itar # indexes hello.tar
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 build photos.itar # discovers photos-0.tar, photos-1.tar, ...
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 build <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`. |
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
- `itar` builds constant-time indexes for one or more TAR shards so you can seek directly to a member without extracting the archive. The project ships a small CLI (`itar`) and a Python helper (`IndexedTarFile`) plus a module-level `itar.open()` convenience that mirrors `tarfile.open()`.
3
+ [![image](https://img.shields.io/pypi/v/itar.svg)](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 build hello.itar # indexes hello.tar
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 build photos.itar # discovers photos-0.tar, photos-1.tar, ...
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 build <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`. |
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "itar"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- build_parser = index_subparsers.add_parser(
20
- "build", help="Create an index file from one or more TAR shards"
39
+ create_parser = index_subparsers.add_parser(
40
+ "create", help="Create an index file from one or more TAR shards"
21
41
  )
22
- build_parser.add_argument("index", type=Path, help="Destination .itar index file")
23
- build_parser.add_argument(
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
- build_parser.add_argument(
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
- build_parser.add_argument(
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
- build_parser.set_defaults(progress=True, func=_cmd_index_build)
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
- "-H",
68
- "--human-readable",
87
+ "--bytes",
69
88
  action="store_true",
70
- help="Render sizes in human units",
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 _resolve_shards_for_build(
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 _cmd_index_build(args) -> None:
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 = _resolve_shards_for_build(
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.human_readable:
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
- return
185
-
186
- for member in current_index:
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