itar 0.1.0__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.
- itar/__init__.py +5 -0
- itar/py.typed +0 -0
- itar/sharded_indexed_tar.py +256 -0
- itar/utils.py +230 -0
- itar-0.1.0.dist-info/METADATA +30 -0
- itar-0.1.0.dist-info/RECORD +8 -0
- itar-0.1.0.dist-info/WHEEL +4 -0
- itar-0.1.0.dist-info/entry_points.txt +3 -0
itar/__init__.py
ADDED
itar/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from io import BufferedReader
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tarfile import TarInfo
|
|
6
|
+
from typing import IO, Callable
|
|
7
|
+
|
|
8
|
+
from .utils import (
|
|
9
|
+
TarFileSectionIO,
|
|
10
|
+
TarIndexError,
|
|
11
|
+
TarMember,
|
|
12
|
+
ThreadSafeFileIO,
|
|
13
|
+
build_tar_index,
|
|
14
|
+
check_tar_index,
|
|
15
|
+
tar_file_info,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ShardedTarIndex = dict[str, (int, TarMember)] # fname -> (shard_idx, TarMember)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShardedIndexedTar(Mapping):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
shards: list[str | os.PathLike | IO[bytes]],
|
|
25
|
+
index: ShardedTarIndex | None = None,
|
|
26
|
+
open_fn: Callable[[str | os.PathLike], IO[bytes]] = None,
|
|
27
|
+
buffered_file_reader: bool = True,
|
|
28
|
+
progress_bar: bool = False,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
For best performance, we recommend using unbuffered shard IO in combination with buffered individual file section readers.
|
|
32
|
+
In our (limited) benchmarks, this was the fastest configuration for reading many small files.
|
|
33
|
+
It it possbile that in other scenarios, buffered shard IO and unbuffered file section readers are faster.
|
|
34
|
+
"""
|
|
35
|
+
self._needs_open = [isinstance(s, str | os.PathLike) for s in shards]
|
|
36
|
+
self._file_reader = BufferedReader if buffered_file_reader else lambda x: x
|
|
37
|
+
open_fn = (
|
|
38
|
+
open_fn or ThreadSafeFileIO
|
|
39
|
+
) # In our benchmarks, `ThreadSafeFileIO` is even faster than `partial(open, mode="rb", buffering=0)`. Likely due to `pread` being fewer syscalls than `seek` + `read`.
|
|
40
|
+
self._shard_file_objs: IO[bytes] = [
|
|
41
|
+
open_fn(tar) if needs_open else tar
|
|
42
|
+
for tar, needs_open in zip(shards, self._needs_open, strict=True)
|
|
43
|
+
]
|
|
44
|
+
if progress_bar:
|
|
45
|
+
from tqdm import tqdm
|
|
46
|
+
else:
|
|
47
|
+
tqdm = lambda x, **kwargs: x # noqa: E731
|
|
48
|
+
self._index = (
|
|
49
|
+
index
|
|
50
|
+
if index is not None
|
|
51
|
+
else {
|
|
52
|
+
name: (i, member)
|
|
53
|
+
for i, file_obj in enumerate(
|
|
54
|
+
tqdm(self._shard_file_objs, desc="Building index", unit="shard")
|
|
55
|
+
)
|
|
56
|
+
for name, member in build_tar_index(file_obj).items()
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def open(
|
|
62
|
+
cls,
|
|
63
|
+
path: str | os.PathLike,
|
|
64
|
+
shards: list[str | os.PathLike] | None = None,
|
|
65
|
+
open_fn: Callable[[str | os.PathLike], IO[bytes]] = None,
|
|
66
|
+
buffered_file_reader: bool = True,
|
|
67
|
+
):
|
|
68
|
+
import msgpack
|
|
69
|
+
|
|
70
|
+
path = Path(path)
|
|
71
|
+
with open(path, "rb") as f:
|
|
72
|
+
num_shards, index = msgpack.load(f)
|
|
73
|
+
return cls(
|
|
74
|
+
shards
|
|
75
|
+
if shards is not None
|
|
76
|
+
else [cls.shard_path(path, num_shards, i) for i in range(num_shards)],
|
|
77
|
+
index,
|
|
78
|
+
open_fn=open_fn,
|
|
79
|
+
buffered_file_reader=buffered_file_reader,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def save(self, path: str | os.PathLike):
|
|
83
|
+
import msgpack
|
|
84
|
+
|
|
85
|
+
path = Path(path)
|
|
86
|
+
with open(path, "wb") as f:
|
|
87
|
+
msgpack.dump((len(self._shard_file_objs), self.index), f)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def shard_path(path: str | os.PathLike, num_shards: int, shard_idx: int) -> Path:
|
|
91
|
+
path = Path(path)
|
|
92
|
+
return path.parent / f"{path.stem}-{shard_idx:0{len(str(num_shards - 1))}d}.tar"
|
|
93
|
+
|
|
94
|
+
def file(self, name: str) -> IO[bytes]:
|
|
95
|
+
i, member = self._index[name]
|
|
96
|
+
_, offset_data, size = member
|
|
97
|
+
if isinstance(size, str):
|
|
98
|
+
return self.file(size) # symlink or hard link
|
|
99
|
+
return self._file_reader(
|
|
100
|
+
TarFileSectionIO(self._shard_file_objs[i], offset_data, size)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def info(self, name: str) -> TarInfo:
|
|
104
|
+
i, member = self._index[name]
|
|
105
|
+
offset, _, _ = member
|
|
106
|
+
return tar_file_info(offset, self._shard_file_objs[i])
|
|
107
|
+
|
|
108
|
+
def check_tar_index(self, names: list[str] | None = None):
|
|
109
|
+
for name in names if names is not None else self:
|
|
110
|
+
i, member = self._index[name]
|
|
111
|
+
check_tar_index(name, member, self._shard_file_objs[i])
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def index(self) -> ShardedTarIndex:
|
|
115
|
+
return self._index
|
|
116
|
+
|
|
117
|
+
def close(self):
|
|
118
|
+
for needed_open, file_obj in zip(
|
|
119
|
+
self._needs_open, self._shard_file_objs, strict=True
|
|
120
|
+
):
|
|
121
|
+
if needed_open:
|
|
122
|
+
# only close what we opened
|
|
123
|
+
file_obj.close()
|
|
124
|
+
|
|
125
|
+
def __enter__(self):
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
129
|
+
self.close()
|
|
130
|
+
|
|
131
|
+
def __getitem__(self, name: str):
|
|
132
|
+
return self.file(name)
|
|
133
|
+
|
|
134
|
+
def __contains__(self, name: str) -> bool:
|
|
135
|
+
return name in self._index
|
|
136
|
+
|
|
137
|
+
def __iter__(self):
|
|
138
|
+
return iter(self._index)
|
|
139
|
+
|
|
140
|
+
def __len__(self):
|
|
141
|
+
return len(self._index)
|
|
142
|
+
|
|
143
|
+
def keys(self):
|
|
144
|
+
return self._index.keys()
|
|
145
|
+
|
|
146
|
+
def values(self):
|
|
147
|
+
for name in self._index:
|
|
148
|
+
yield self[name]
|
|
149
|
+
|
|
150
|
+
def items(self):
|
|
151
|
+
for name in self._index:
|
|
152
|
+
yield (name, self[name])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cli():
|
|
156
|
+
import argparse
|
|
157
|
+
|
|
158
|
+
parser = argparse.ArgumentParser(description="Create or check an itar index.")
|
|
159
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
160
|
+
|
|
161
|
+
create_parser = subparsers.add_parser("create", help="Create an itar index")
|
|
162
|
+
create_parser.add_argument(
|
|
163
|
+
"itar", type=Path, help="Path to the new itar index that will be created"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
check_parser = subparsers.add_parser("check", help="Check an existing itar index")
|
|
167
|
+
check_parser.add_argument("itar", type=Path, help="Path to the itar index")
|
|
168
|
+
|
|
169
|
+
ls_parser = subparsers.add_parser("ls", help="List files in an itar index")
|
|
170
|
+
ls_parser.add_argument("itar", type=Path, help="Path to the itar index")
|
|
171
|
+
ls_parser.add_argument(
|
|
172
|
+
"-l", "--long", action="store_true", help="Show long listing format"
|
|
173
|
+
)
|
|
174
|
+
ls_parser.add_argument(
|
|
175
|
+
"-H", "--human-readable", action="store_true", help="Use human-readable sizes"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
if args.command == "create":
|
|
181
|
+
_create(args)
|
|
182
|
+
elif args.command == "check":
|
|
183
|
+
_check(args)
|
|
184
|
+
elif args.command == "ls":
|
|
185
|
+
_ls(args)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _create(args):
|
|
189
|
+
import re
|
|
190
|
+
|
|
191
|
+
itar_path = Path(args.itar)
|
|
192
|
+
stem = itar_path.stem
|
|
193
|
+
pattern = re.compile(rf"^{re.escape(stem)}-\d+\.tar$")
|
|
194
|
+
shards = sorted(
|
|
195
|
+
[
|
|
196
|
+
s
|
|
197
|
+
for s in itar_path.parent.glob(f"{itar_path.stem}-*.tar")
|
|
198
|
+
if s.is_file() and pattern.match(s.name)
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
num_shards = len(shards)
|
|
202
|
+
if num_shards < 1:
|
|
203
|
+
print(
|
|
204
|
+
f"No shards found for {itar_path}.\n"
|
|
205
|
+
f"Please create shard files first. Expected pattern: "
|
|
206
|
+
f"'{itar_path.stem}-NN.tar' (where NN is the zero-padded shard index, starting from 0)."
|
|
207
|
+
)
|
|
208
|
+
exit(1)
|
|
209
|
+
|
|
210
|
+
# ensure shards are named correctly
|
|
211
|
+
expected = [
|
|
212
|
+
ShardedIndexedTar.shard_path(itar_path, num_shards, i)
|
|
213
|
+
for i in range(num_shards)
|
|
214
|
+
]
|
|
215
|
+
assert shards == expected, (
|
|
216
|
+
f"Shards do not match expected names: {shards} != {expected}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
with ShardedIndexedTar(shards, progress_bar=True) as itar:
|
|
220
|
+
itar.save(itar_path)
|
|
221
|
+
print(f"Created itar index at {itar_path} with {num_shards} shards.")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _check(args):
|
|
225
|
+
from tqdm import tqdm
|
|
226
|
+
|
|
227
|
+
did_error = False
|
|
228
|
+
|
|
229
|
+
with ShardedIndexedTar.open(args.itar) as itar:
|
|
230
|
+
for member in tqdm(itar, desc="Checking files", unit="file"):
|
|
231
|
+
try:
|
|
232
|
+
itar.check_tar_index([member])
|
|
233
|
+
except TarIndexError as e:
|
|
234
|
+
print(e)
|
|
235
|
+
did_error = True
|
|
236
|
+
|
|
237
|
+
if did_error:
|
|
238
|
+
exit(1)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _ls(args):
|
|
242
|
+
with ShardedIndexedTar.open(args.itar) as itar:
|
|
243
|
+
index = itar.index
|
|
244
|
+
if args.long:
|
|
245
|
+
for member, (shard_idx, (offset, offset_data, size)) in index.items():
|
|
246
|
+
if args.human_readable:
|
|
247
|
+
from humanize import naturalsize
|
|
248
|
+
|
|
249
|
+
size = naturalsize(size, gnu=True)
|
|
250
|
+
print(
|
|
251
|
+
f"{member:<40} {shard_idx:>5} {offset:>12} {offset_data:>12} {size:>10}"
|
|
252
|
+
)
|
|
253
|
+
print(f"{'NAME':<40} {'SHARD':>5} {'OFFSET':>12} {'OFF_DATA':>12} {'SIZE':>10}")
|
|
254
|
+
else:
|
|
255
|
+
for member in index:
|
|
256
|
+
print(member)
|
itar/utils.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import tarfile
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
from tarfile import TarFile, TarInfo
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from typing import IO, BinaryIO
|
|
9
|
+
|
|
10
|
+
TarMember = tuple[int, int, int | str] # (offset, offset_data, size | linkname)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def tar_file_info(offset: int, file_obj: IO[bytes]) -> TarInfo:
|
|
14
|
+
file_obj.seek(offset)
|
|
15
|
+
return TarInfo.fromtarfile(
|
|
16
|
+
# want to avoid creating a new TarFile instance (potentially slow)
|
|
17
|
+
SimpleNamespace(
|
|
18
|
+
fileobj=file_obj,
|
|
19
|
+
# would be the defaults after TarFile.__init__
|
|
20
|
+
encoding=TarFile.encoding,
|
|
21
|
+
errors="surrogateescape",
|
|
22
|
+
pax_headers={},
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def tarinfo2member(tarinfo: TarInfo) -> TarMember:
|
|
28
|
+
if tarinfo.issym():
|
|
29
|
+
size = os.path.normpath(
|
|
30
|
+
"/".join(filter(None, (os.path.dirname(tarinfo.name), tarinfo.linkname)))
|
|
31
|
+
)
|
|
32
|
+
elif tarinfo.islnk():
|
|
33
|
+
size = os.path.normpath(tarinfo.linkname)
|
|
34
|
+
else:
|
|
35
|
+
size = tarinfo.size
|
|
36
|
+
|
|
37
|
+
if tarinfo.sparse is not None:
|
|
38
|
+
raise NotImplementedError("Sparse files are not supported")
|
|
39
|
+
|
|
40
|
+
return (tarinfo.offset, tarinfo.offset_data, size)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_tar_index(
|
|
44
|
+
tar: str | os.PathLike | IO[bytes] | TarFile,
|
|
45
|
+
) -> dict[str, TarMember]:
|
|
46
|
+
if isinstance(tar, str | os.PathLike):
|
|
47
|
+
tar = tarfile.open(tar, "r:")
|
|
48
|
+
elif isinstance(tar, TarFile):
|
|
49
|
+
tar = nullcontext(tar)
|
|
50
|
+
else:
|
|
51
|
+
tar.seek(0)
|
|
52
|
+
tar = tarfile.open(fileobj=tar, mode="r:")
|
|
53
|
+
|
|
54
|
+
with tar as f:
|
|
55
|
+
members = {member.name: member for member in f.getmembers()}
|
|
56
|
+
return {
|
|
57
|
+
member.name: tarinfo2member(member)
|
|
58
|
+
for member in members.values()
|
|
59
|
+
if (
|
|
60
|
+
# index only includes files and links. no directories, devices, etc.
|
|
61
|
+
member.isreg()
|
|
62
|
+
or (
|
|
63
|
+
member.type not in tarfile.SUPPORTED_TYPES
|
|
64
|
+
) # Members with unknown types are treated as regular files.
|
|
65
|
+
or member.issym()
|
|
66
|
+
or member.islnk()
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TarIndexError(Exception):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_tar_index(
|
|
76
|
+
name: str,
|
|
77
|
+
tar_offset: TarMember,
|
|
78
|
+
file_obj: IO[bytes],
|
|
79
|
+
):
|
|
80
|
+
offset, offset_data, size = tar_offset
|
|
81
|
+
info = tar_file_info(offset, file_obj)
|
|
82
|
+
if (
|
|
83
|
+
info.offset != offset
|
|
84
|
+
or info.name != name
|
|
85
|
+
or info.offset_data != offset_data
|
|
86
|
+
or (info.size != size and not info.islnk() and not info.issym())
|
|
87
|
+
):
|
|
88
|
+
# TODO: if link, check that linkname is correct
|
|
89
|
+
raise TarIndexError(
|
|
90
|
+
f"Index mismatch: "
|
|
91
|
+
f"expected ({name}, {offset}, {offset_data}, {size}), "
|
|
92
|
+
f"got ({info.name}, {info.offset}, {info.offset_data}, {info.size})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ThreadSafeFileIO(io.RawIOBase):
|
|
97
|
+
"""
|
|
98
|
+
A thread-safe, file-like object that wraps a file descriptor
|
|
99
|
+
and uses os.pread() for concurrent reads.
|
|
100
|
+
|
|
101
|
+
Each thread has its own seek position.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, path: str | os.PathLike):
|
|
105
|
+
self._path = str(path)
|
|
106
|
+
self._fd = os.open(path, os.O_RDONLY)
|
|
107
|
+
self._local = threading.local()
|
|
108
|
+
self._closed = False
|
|
109
|
+
|
|
110
|
+
def _get_pos(self) -> int:
|
|
111
|
+
return getattr(self._local, "pos", 0)
|
|
112
|
+
|
|
113
|
+
def _set_pos(self, val: int) -> None:
|
|
114
|
+
self._local.pos = val
|
|
115
|
+
|
|
116
|
+
def seek(self, offset: int, whence: int = os.SEEK_SET) -> int:
|
|
117
|
+
pos = self._get_pos()
|
|
118
|
+
if whence == os.SEEK_SET:
|
|
119
|
+
self._set_pos(offset)
|
|
120
|
+
elif whence == os.SEEK_CUR:
|
|
121
|
+
self._set_pos(pos + offset)
|
|
122
|
+
elif whence == os.SEEK_END:
|
|
123
|
+
end = os.lseek(self._fd, 0, os.SEEK_END)
|
|
124
|
+
self._set_pos(end + offset)
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"Invalid whence: {whence}")
|
|
127
|
+
return self._get_pos()
|
|
128
|
+
|
|
129
|
+
def tell(self) -> int:
|
|
130
|
+
return self._get_pos()
|
|
131
|
+
|
|
132
|
+
def read(self, size: int = -1) -> bytes:
|
|
133
|
+
if size < 0:
|
|
134
|
+
raise NotImplementedError("read(size=-1) not supported")
|
|
135
|
+
pos = self._get_pos()
|
|
136
|
+
data = os.pread(self._fd, size, pos)
|
|
137
|
+
self._set_pos(pos + len(data))
|
|
138
|
+
return data
|
|
139
|
+
|
|
140
|
+
def readable(self) -> bool:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
def seekable(self) -> bool:
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def writable(self) -> bool:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def close(self) -> None:
|
|
150
|
+
if not self._closed:
|
|
151
|
+
os.close(self._fd)
|
|
152
|
+
self._closed = True
|
|
153
|
+
|
|
154
|
+
def fileno(self) -> int:
|
|
155
|
+
return self._fd
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def name(self) -> str: # optional
|
|
159
|
+
return self._path
|
|
160
|
+
|
|
161
|
+
def __enter__(self):
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(self, exc_type, exc_value, tb):
|
|
165
|
+
self.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TarFileSectionIO(io.RawIOBase):
|
|
169
|
+
def __init__(self, fileobj: BinaryIO, offset: int, size: int):
|
|
170
|
+
self._fileobj = fileobj
|
|
171
|
+
self._start = offset
|
|
172
|
+
self._end = offset + size
|
|
173
|
+
self._pos = 0 # relative position within the slice
|
|
174
|
+
|
|
175
|
+
def read(self, size: int = -1) -> bytes:
|
|
176
|
+
if self._pos >= self._end - self._start:
|
|
177
|
+
return b""
|
|
178
|
+
|
|
179
|
+
abs_pos = self._start + self._pos
|
|
180
|
+
max_len = self._end - (self._start + self._pos)
|
|
181
|
+
|
|
182
|
+
if size < 0 or size > max_len:
|
|
183
|
+
size = max_len
|
|
184
|
+
|
|
185
|
+
self._fileobj.seek(abs_pos)
|
|
186
|
+
data = self._fileobj.read(size)
|
|
187
|
+
self._pos += len(data)
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
def readinto(self, b: bytearray) -> int:
|
|
191
|
+
data = self.read(len(b))
|
|
192
|
+
n = len(data)
|
|
193
|
+
b[:n] = data
|
|
194
|
+
return n
|
|
195
|
+
|
|
196
|
+
def readall(self) -> bytes:
|
|
197
|
+
return self.read(-1)
|
|
198
|
+
|
|
199
|
+
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
|
200
|
+
if whence == io.SEEK_SET:
|
|
201
|
+
new_pos = offset
|
|
202
|
+
elif whence == io.SEEK_CUR:
|
|
203
|
+
new_pos = self._pos + offset
|
|
204
|
+
elif whence == io.SEEK_END:
|
|
205
|
+
new_pos = (self._end - self._start) + offset
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"Invalid whence: {whence}")
|
|
208
|
+
|
|
209
|
+
if new_pos < 0:
|
|
210
|
+
raise ValueError("Negative seek position")
|
|
211
|
+
self._pos = min(new_pos, self._end - self._start)
|
|
212
|
+
return self._pos
|
|
213
|
+
|
|
214
|
+
def tell(self) -> int:
|
|
215
|
+
return self._pos
|
|
216
|
+
|
|
217
|
+
def readable(self) -> bool:
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def seekable(self) -> bool:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
def writable(self) -> bool:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
def close(self) -> None:
|
|
227
|
+
pass # Don't close the underlying file
|
|
228
|
+
|
|
229
|
+
def __len__(self) -> int:
|
|
230
|
+
return self._end - self._start
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: itar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Karim Abou Zeid
|
|
6
|
+
Author-email: Karim Abou Zeid <contact@ka.codes>
|
|
7
|
+
Requires-Dist: humanize>=4.12.3
|
|
8
|
+
Requires-Dist: msgpack>=1.1.1
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# itar
|
|
13
|
+
|
|
14
|
+
## Minimal example
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
echo Hello world! > bar.txt
|
|
18
|
+
tar cf foo-0.tar bar.txt # create regular tar file(s) with zero-padded shard number
|
|
19
|
+
itar create foo.itar # create the index
|
|
20
|
+
itar ls foo.itar # (optional) view the contents of the index
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Now you can open and efficiently access the archived files in python in constant time
|
|
24
|
+
```python
|
|
25
|
+
from itar import ShardedIndexedTar
|
|
26
|
+
|
|
27
|
+
with ShardedIndexedTar.open("foo.itar") as itar:
|
|
28
|
+
f = itar.file("bar.txt") # file-like object
|
|
29
|
+
assert f.read().decode("utf-8") == "Hello world!\n"
|
|
30
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
itar/__init__.py,sha256=1G91ozs-YBv-lnHeStSoDk6HTye_HLkIrLn_fQjSVt4,91
|
|
2
|
+
itar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
itar/sharded_indexed_tar.py,sha256=G3k4kD4_5vakAzsBC7RfZ0YGPk2FuiDtmsE08fxAZG8,8141
|
|
4
|
+
itar/utils.py,sha256=iduueK8wfnEBwd1Tcyt6z544gpJYA62YXPI9jdc1Xbc,6453
|
|
5
|
+
itar-0.1.0.dist-info/WHEEL,sha256=I8-bO5cg2sb8TH6ZM6EgCP87Y1cV_f9UGgWnfAhVOZI,78
|
|
6
|
+
itar-0.1.0.dist-info/entry_points.txt,sha256=uRZGXYbybs8I9NbVn6dODCzi30Z5BEEZ3b6bR93ot1E,55
|
|
7
|
+
itar-0.1.0.dist-info/METADATA,sha256=x-IiG0GfVw5hoQ8JI2rGqslRIYNSuD-ygolTxAJAHL8,830
|
|
8
|
+
itar-0.1.0.dist-info/RECORD,,
|