multiarchive 0.1.2__tar.gz → 0.1.4__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.
- {multiarchive-0.1.2 → multiarchive-0.1.4}/PKG-INFO +276 -261
- {multiarchive-0.1.2 → multiarchive-0.1.4}/README.md +275 -260
- multiarchive-0.1.4/pyproject.toml +117 -0
- multiarchive-0.1.2/pyproject.toml → multiarchive-0.1.4/pyproject.toml.orig +2 -2
- {multiarchive-0.1.2 → multiarchive-0.1.4}/src/multiarchive/__init__.py +3 -3
- {multiarchive-0.1.2 → multiarchive-0.1.4}/src/multiarchive/_archive.py +825 -765
|
@@ -1,269 +1,284 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: multiarchive
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A high level archive handler for Python.
|
|
5
5
|
Author: NSPC911
|
|
6
6
|
Author-email: NSPC911 <87571998+NSPC911@users.noreply.github.com>
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
|
|
10
|
-
# multiarchive
|
|
11
|
-
|
|
12
|
-
A high level abstraction of multiple archive formats
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
uv add multiarchive
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Optional dependency for RAR support:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
uv add rarfile
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
ZStandard support requires the `backports-zstd` package on Python <3.14:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
uv add "backports-zstd; python_version < '3.14'"
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Quick Start
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
from multiarchive import Archive
|
|
36
|
-
|
|
37
|
-
# Open and inspect
|
|
38
|
-
with Archive("archive.zip") as arc:
|
|
39
|
-
print(arc.namelist()) # list of member names
|
|
40
|
-
print(len(arc)) # number of members
|
|
41
|
-
print(arc.size) # total uncompressed size in bytes
|
|
42
|
-
|
|
43
|
-
# Extract a single file
|
|
44
|
-
with Archive("archive.tar.gz") as arc:
|
|
45
|
-
arc.extract("docs/readme.md", path="./output")
|
|
46
|
-
|
|
47
|
-
# Read file contents without extracting
|
|
48
|
-
with Archive("archive.zip") as arc:
|
|
49
|
-
f = arc.open("config.json")
|
|
50
|
-
content = f.read()
|
|
51
|
-
f.close()
|
|
52
|
-
|
|
53
|
-
# Iterate over members
|
|
54
|
-
with Archive("archive.zip") as arc:
|
|
55
|
-
for name in arc:
|
|
56
|
-
if arc.is_file(name):
|
|
57
|
-
print(f" {name}")
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Supported Formats
|
|
61
|
-
|
|
62
|
-
| Format | Read | Write | Extensions |
|
|
63
|
-
| ------- | ---- | ----- | --------------------------- |
|
|
64
|
-
| ZIP | Yes | Yes | `.zip` |
|
|
65
|
-
| TAR | Yes | Yes | `.tar` |
|
|
66
|
-
| TAR.GZ | Yes | Yes | `.tgz`, `.tar.gz` |
|
|
67
|
-
| TAR.BZ2 | Yes | Yes | `.tbz`, `.tbz2`, `.tar.bz2` |
|
|
68
|
-
| TAR.XZ | Yes | Yes | `.tar.xz`, `.tar.lzma` |
|
|
69
|
-
| TAR.ZST | Yes | Yes | `.tzst`, `.tar.zst` |
|
|
70
|
-
| RAR | Yes | No | `.rar` |
|
|
71
|
-
|
|
72
|
-
RAR support requires the `rarfile` package and an external `unrar` tool.
|
|
73
|
-
|
|
74
|
-
## Reading Archives
|
|
75
|
-
|
|
76
|
-
### Opening
|
|
77
|
-
|
|
78
|
-
Use the context manager (recommended):
|
|
79
|
-
|
|
80
|
-
```python
|
|
81
|
-
with Archive("file.zip") as arc:
|
|
82
|
-
...
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Or the classmethod:
|
|
86
|
-
|
|
87
|
-
```python
|
|
88
|
-
arc = Archive.open_archive("file.zip")
|
|
89
|
-
try:
|
|
90
|
-
arc.namelist()
|
|
91
|
-
finally:
|
|
92
|
-
arc.close()
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Reading is supported on all formats, but password-protected archives are not supported and will raise a `ValueError`.
|
|
96
|
-
If `algo` isnt explicitly provided, it will attempt to open it as ZIP first, then TAR, then RAR (if `rarfile` is installed). If all fails, it raises a ValueError.
|
|
97
|
-
|
|
98
|
-
### Listing Members
|
|
99
|
-
|
|
100
|
-
```python
|
|
101
|
-
with Archive("file.zip") as arc:
|
|
102
|
-
names = arc.namelist() # Just names
|
|
103
|
-
names = arc.members # alias to namelist
|
|
104
|
-
|
|
105
|
-
# extra info (ArchiveMemberInfo objects)
|
|
106
|
-
for info in arc.infolist():
|
|
107
|
-
print(f"{info.name}: {info.uncompressed_size} bytes, mtime={info.mtime}")
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Checking Membership
|
|
111
|
-
|
|
112
|
-
```python
|
|
113
|
-
with Archive("file.zip") as arc:
|
|
114
|
-
if "config.json" in arc:
|
|
115
|
-
print("Found!")
|
|
116
|
-
|
|
117
|
-
for name in arc:
|
|
118
|
-
if arc.is_file(name):
|
|
119
|
-
...
|
|
120
|
-
if arc.is_dir(name):
|
|
121
|
-
...
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Extracting
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
with Archive("file.tar.gz") as arc:
|
|
128
|
-
# Single file
|
|
129
|
-
arc.extract("src/main.py", path="./extracted")
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Reading Member Contents
|
|
133
|
-
|
|
134
|
-
```python
|
|
135
|
-
with Archive("file.zip") as arc:
|
|
136
|
-
f = arc.open("data.json")
|
|
137
|
-
if f is not None:
|
|
138
|
-
content = f.read()
|
|
139
|
-
f.close()
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Writing Archives
|
|
143
|
-
|
|
144
|
-
### Creating a New Archive
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
from multiarchive import Archive
|
|
148
|
-
|
|
149
|
-
# ZIP
|
|
150
|
-
with Archive("output.zip", mode="w") as arc:
|
|
151
|
-
...
|
|
152
|
-
|
|
153
|
-
# TAR.GZ with compression level
|
|
154
|
-
with Archive("output.tar.gz", mode="w", compression_level=9) as arc:
|
|
155
|
-
...
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
```python
|
|
161
|
-
with Archive("
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
| `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
10
|
+
# multiarchive
|
|
11
|
+
|
|
12
|
+
A high level abstraction of multiple archive formats
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add multiarchive
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optional dependency for RAR support:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add rarfile
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
ZStandard support requires the `backports-zstd` package on Python <3.14:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add "backports-zstd; python_version < '3.14'"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from multiarchive import Archive
|
|
36
|
+
|
|
37
|
+
# Open and inspect
|
|
38
|
+
with Archive("archive.zip") as arc:
|
|
39
|
+
print(arc.namelist()) # list of member names
|
|
40
|
+
print(len(arc)) # number of members
|
|
41
|
+
print(arc.size) # total uncompressed size in bytes
|
|
42
|
+
|
|
43
|
+
# Extract a single file
|
|
44
|
+
with Archive("archive.tar.gz") as arc:
|
|
45
|
+
arc.extract("docs/readme.md", path="./output")
|
|
46
|
+
|
|
47
|
+
# Read file contents without extracting
|
|
48
|
+
with Archive("archive.zip") as arc:
|
|
49
|
+
f = arc.open("config.json")
|
|
50
|
+
content = f.read()
|
|
51
|
+
f.close()
|
|
52
|
+
|
|
53
|
+
# Iterate over members
|
|
54
|
+
with Archive("archive.zip") as arc:
|
|
55
|
+
for name in arc:
|
|
56
|
+
if arc.is_file(name):
|
|
57
|
+
print(f" {name}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Supported Formats
|
|
61
|
+
|
|
62
|
+
| Format | Read | Write | Extensions |
|
|
63
|
+
| ------- | ---- | ----- | --------------------------- |
|
|
64
|
+
| ZIP | Yes | Yes | `.zip` |
|
|
65
|
+
| TAR | Yes | Yes | `.tar` |
|
|
66
|
+
| TAR.GZ | Yes | Yes | `.tgz`, `.tar.gz` |
|
|
67
|
+
| TAR.BZ2 | Yes | Yes | `.tbz`, `.tbz2`, `.tar.bz2` |
|
|
68
|
+
| TAR.XZ | Yes | Yes | `.tar.xz`, `.tar.lzma` |
|
|
69
|
+
| TAR.ZST | Yes | Yes | `.tzst`, `.tar.zst` |
|
|
70
|
+
| RAR | Yes | No | `.rar` |
|
|
71
|
+
|
|
72
|
+
RAR support requires the `rarfile` package and an external `unrar` tool.
|
|
73
|
+
|
|
74
|
+
## Reading Archives
|
|
75
|
+
|
|
76
|
+
### Opening
|
|
77
|
+
|
|
78
|
+
Use the context manager (recommended):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
with Archive("file.zip") as arc:
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or the classmethod:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
arc = Archive.open_archive("file.zip")
|
|
89
|
+
try:
|
|
90
|
+
arc.namelist()
|
|
91
|
+
finally:
|
|
92
|
+
arc.close()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Reading is supported on all formats, but password-protected archives are not supported and will raise a `ValueError`.
|
|
96
|
+
If `algo` isnt explicitly provided, it will attempt to open it as ZIP first, then TAR, then RAR (if `rarfile` is installed). If all fails, it raises a ValueError.
|
|
97
|
+
|
|
98
|
+
### Listing Members
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
with Archive("file.zip") as arc:
|
|
102
|
+
names = arc.namelist() # Just names
|
|
103
|
+
names = arc.members # alias to namelist
|
|
104
|
+
|
|
105
|
+
# extra info (ArchiveMemberInfo objects)
|
|
106
|
+
for info in arc.infolist():
|
|
107
|
+
print(f"{info.name}: {info.uncompressed_size} bytes, mtime={info.mtime}")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Checking Membership
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
with Archive("file.zip") as arc:
|
|
114
|
+
if "config.json" in arc:
|
|
115
|
+
print("Found!")
|
|
116
|
+
|
|
117
|
+
for name in arc:
|
|
118
|
+
if arc.is_file(name):
|
|
119
|
+
...
|
|
120
|
+
if arc.is_dir(name):
|
|
121
|
+
...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Extracting
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with Archive("file.tar.gz") as arc:
|
|
128
|
+
# Single file
|
|
129
|
+
arc.extract("src/main.py", path="./extracted")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Reading Member Contents
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
with Archive("file.zip") as arc:
|
|
136
|
+
f = arc.open("data.json")
|
|
137
|
+
if f is not None:
|
|
138
|
+
content = f.read()
|
|
139
|
+
f.close()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Writing Archives
|
|
143
|
+
|
|
144
|
+
### Creating a New Archive
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from multiarchive import Archive
|
|
148
|
+
|
|
149
|
+
# ZIP
|
|
150
|
+
with Archive("output.zip", mode="w") as arc:
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
# TAR.GZ with compression level
|
|
154
|
+
with Archive("output.tar.gz", mode="w", compression_level=9) as arc:
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Add members with an optional name to use inside the archive:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
with Archive("output.zip", mode="w") as arc:
|
|
162
|
+
arc.add("src/main.py", arcname="main.py")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
ZIP timestamps are limited to the years 1980 through 2107. To clamp timestamps
|
|
166
|
+
outside that range instead of raising an error:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
with Archive("output.zip", mode="w", strict_timestamps=False) as arc:
|
|
170
|
+
arc.add("old-file.txt")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The format is determined by file extension. Use the `algo` parameter to override:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
with Archive("my_archive", mode="w", algo="tar.xz") as arc:
|
|
177
|
+
...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Compression Levels
|
|
181
|
+
|
|
182
|
+
| Format | Range | Default |
|
|
183
|
+
| ------- | ----- | ------- |
|
|
184
|
+
| ZIP | 0–9 | 6 |
|
|
185
|
+
| TAR.GZ | 0–9 | 6 |
|
|
186
|
+
| TAR.BZ2 | 1–9 | 9 |
|
|
187
|
+
| TAR.XZ | 0–9 | 6 |
|
|
188
|
+
| TAR.ZST | 1–22 | 3 |
|
|
189
|
+
|
|
190
|
+
## File Information Object
|
|
191
|
+
|
|
192
|
+
The `infolist()` method returns `ArchiveMemberInfo` objects with a unified interface across all formats:
|
|
193
|
+
|
|
194
|
+
| Field | Type | Description |
|
|
195
|
+
| ------------------- | ------------------------------- | -------------------------------------- |
|
|
196
|
+
| `name` | `str` | Member filename |
|
|
197
|
+
| `uncompressed_size` | `int` | Original file size in bytes |
|
|
198
|
+
| `compressed_size` | `int \| None` | Compressed size (`None` for TAR) |
|
|
199
|
+
| `mtime` | `float` | Modification time as epoch seconds |
|
|
200
|
+
| `mode` | `int \| None` | Unix permission bits |
|
|
201
|
+
| `is_dir` | `bool` | Whether the member is a directory |
|
|
202
|
+
| `raw` | `ZipInfo \| TarInfo \| RarInfo` | Original backend object (escape hatch) |
|
|
203
|
+
|
|
204
|
+
Factory methods for direct conversion (you shouldn't need these in normal usage):
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
info = ArchiveMemberInfo.from_zipinfo(zip_info)
|
|
208
|
+
info = ArchiveMemberInfo.from_tarinfo(tar_info)
|
|
209
|
+
info = ArchiveMemberInfo.from_rarinfo(rar_info)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Properties
|
|
213
|
+
|
|
214
|
+
| Method/Property | Description |
|
|
215
|
+
| ----------------------- | --------------------------------------------------------------- |
|
|
216
|
+
| `members` | Alias for `namelist()` |
|
|
217
|
+
| `size` | Total uncompressed size of all members |
|
|
218
|
+
| `comment` / `comment=` | Get/set archive comment (ZIP only, raises ValueError otherwise) |
|
|
219
|
+
|
|
220
|
+
## Error Handling
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from multiarchive import Archive, BadArchiveError
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
with Archive("corrupted.zip") as arc:
|
|
227
|
+
arc.namelist()
|
|
228
|
+
except BadArchiveError as e:
|
|
229
|
+
print(f"Archive error: {e}")
|
|
230
|
+
except NotImplementedError as e:
|
|
231
|
+
# password-protected (not supported)
|
|
232
|
+
print(f"Not implemented: {e}")
|
|
233
|
+
except ValueError as e:
|
|
234
|
+
# Unknown format
|
|
235
|
+
print(f"Value error: {e}")
|
|
236
|
+
except FileNotFoundError:
|
|
237
|
+
print("File not found")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## API Reference
|
|
241
|
+
|
|
242
|
+
### `Archive`
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
Archive(
|
|
246
|
+
filename: str | Path,
|
|
247
|
+
mode: str = "r",
|
|
248
|
+
compression_level: int | None = None,
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| Parameter | Description |
|
|
253
|
+
| ------------------- | ------------------------------------------------- |
|
|
254
|
+
| `filename` | Path to the archive file |
|
|
255
|
+
| `mode` | `'r'` for read, `'w'` for write, `'a'` for append |
|
|
256
|
+
| `compression_level` | Compression level (format-dependent) |
|
|
257
|
+
|
|
258
|
+
#### Methods
|
|
259
|
+
|
|
260
|
+
| Method | Description |
|
|
261
|
+
| ------------------------------------------------------ | -------------------------------------- |
|
|
262
|
+
| `open_archive(cls, filename, mode, compression_level)` | Factory method, returns opened archive |
|
|
263
|
+
| `namelist()` | List of member names |
|
|
264
|
+
| `infolist()` | List of `ArchiveMemberInfo` objects |
|
|
265
|
+
| `extract(member, path)` | Extract single member |
|
|
266
|
+
| `open(member, mode)` | Open member as file-like object |
|
|
267
|
+
| `is_dir(member)` | Check if member is a directory |
|
|
268
|
+
| `is_file(member)` | Check if member is a file |
|
|
269
|
+
| `close()` | Explicitly close the archive |
|
|
270
|
+
|
|
271
|
+
### `BadArchiveError`
|
|
272
|
+
|
|
273
|
+
Raised when an archive file is corrupt or in an unsupported format.
|
|
274
|
+
|
|
275
|
+
### `ArchiveExtensions`
|
|
276
|
+
|
|
277
|
+
Frozen dataclass mapping format names to their common file extensions:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
ArchiveExtensions.zip # (".zip",)
|
|
281
|
+
ArchiveExtensions.gz # (".tgz", ".tar.gz")
|
|
282
|
+
ArchiveExtensions.xz # (".tar.xz", ".tar.lzma")
|
|
283
|
+
# ... etc
|
|
284
|
+
```
|