zipwire 0.2.0__tar.gz → 0.4.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.
- {zipwire-0.2.0 → zipwire-0.4.0}/.github/dependabot.yml +2 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/.github/workflows/ci.yml +8 -8
- {zipwire-0.2.0 → zipwire-0.4.0}/.github/workflows/codeql.yml +3 -3
- {zipwire-0.2.0 → zipwire-0.4.0}/.github/workflows/release.yml +4 -4
- {zipwire-0.2.0 → zipwire-0.4.0}/.github/workflows/scorecard.yml +3 -3
- {zipwire-0.2.0 → zipwire-0.4.0}/PKG-INFO +49 -4
- {zipwire-0.2.0 → zipwire-0.4.0}/README.md +47 -2
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/api.rst +23 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/backends.rst +61 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/quickstart.rst +82 -1
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/__init__.py +42 -1
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/__main__.py +1 -1
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_async.py +23 -74
- zipwire-0.4.0/src/zipwire/_base.py +97 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_sync.py +21 -70
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_version.py +2 -2
- zipwire-0.4.0/src/zipwire/_wheel.py +222 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/backends/__init__.py +4 -0
- zipwire-0.4.0/src/zipwire/backends/_file.py +206 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_async.py +25 -24
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_backends.py +149 -2
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_integration.py +11 -13
- zipwire-0.4.0/tests/test_wheel.py +268 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/uv.lock +106 -106
- {zipwire-0.2.0 → zipwire-0.4.0}/.gitignore +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/.readthedocs.yaml +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/AGENTS.md +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/CLAUDE.md +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/LICENSE +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/SECURITY.md +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/Makefile +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/conf.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/index.rst +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/requirements.txt +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/docs/security.rst +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/pyproject.toml +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_constants.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_decompress.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_errors.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_parser.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_types.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/_zipinfo.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/backends/_aiohttp.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/backends/_httpx2.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/backends/_requests.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/backends/_urllib3.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/src/zipwire/py.typed +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/__init__.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/conftest.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_cli.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_decompress.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_parser.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_sync.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tests/test_zipinfo.py +0 -0
- {zipwire-0.2.0 → zipwire-0.4.0}/tox.ini +0 -0
|
@@ -18,10 +18,10 @@ jobs:
|
|
|
18
18
|
permissions:
|
|
19
19
|
contents: read
|
|
20
20
|
steps:
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
22
|
with:
|
|
23
23
|
persist-credentials: false
|
|
24
|
-
- uses: astral-sh/setup-uv@
|
|
24
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
25
25
|
with:
|
|
26
26
|
enable-cache: true
|
|
27
27
|
- run: uvx ruff check src tests
|
|
@@ -37,10 +37,10 @@ jobs:
|
|
|
37
37
|
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
38
38
|
continue-on-error: ${{ matrix.python-version == '3.15' }}
|
|
39
39
|
steps:
|
|
40
|
-
- uses: actions/checkout@
|
|
40
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
41
41
|
with:
|
|
42
42
|
persist-credentials: false
|
|
43
|
-
- uses: astral-sh/setup-uv@
|
|
43
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
44
44
|
with:
|
|
45
45
|
enable-cache: true
|
|
46
46
|
- run: uv tool install --python ${{ matrix.python-version }} tox --with tox-uv --with tox-gh
|
|
@@ -58,10 +58,10 @@ jobs:
|
|
|
58
58
|
permissions:
|
|
59
59
|
contents: read
|
|
60
60
|
steps:
|
|
61
|
-
- uses: actions/checkout@
|
|
61
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
62
62
|
with:
|
|
63
63
|
persist-credentials: false
|
|
64
|
-
- uses: astral-sh/setup-uv@
|
|
64
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
65
65
|
with:
|
|
66
66
|
enable-cache: true
|
|
67
67
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
@@ -78,10 +78,10 @@ jobs:
|
|
|
78
78
|
permissions:
|
|
79
79
|
contents: read
|
|
80
80
|
steps:
|
|
81
|
-
- uses: actions/checkout@
|
|
81
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
82
82
|
with:
|
|
83
83
|
persist-credentials: false
|
|
84
|
-
- uses: astral-sh/setup-uv@
|
|
84
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
85
85
|
with:
|
|
86
86
|
enable-cache: true
|
|
87
87
|
- run: uv sync --group docs
|
|
@@ -16,10 +16,10 @@ jobs:
|
|
|
16
16
|
permissions:
|
|
17
17
|
security-events: write
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
20
|
with:
|
|
21
21
|
persist-credentials: false
|
|
22
|
-
- uses: github/codeql-action/init@
|
|
22
|
+
- uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
|
23
23
|
with:
|
|
24
24
|
languages: python
|
|
25
|
-
- uses: github/codeql-action/analyze@
|
|
25
|
+
- uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
|
@@ -12,12 +12,12 @@ jobs:
|
|
|
12
12
|
permissions:
|
|
13
13
|
contents: read
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
16
16
|
with:
|
|
17
17
|
persist-credentials: false
|
|
18
|
-
- uses: astral-sh/setup-uv@
|
|
18
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
19
19
|
with:
|
|
20
|
-
enable-cache:
|
|
20
|
+
enable-cache: false
|
|
21
21
|
- run: uv build
|
|
22
22
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
23
23
|
with:
|
|
@@ -36,6 +36,6 @@ jobs:
|
|
|
36
36
|
with:
|
|
37
37
|
name: dist
|
|
38
38
|
path: dist/
|
|
39
|
-
- uses: pypa/gh-action-pypi-publish@
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
40
40
|
with:
|
|
41
41
|
attestations: true
|
|
@@ -15,10 +15,10 @@ jobs:
|
|
|
15
15
|
security-events: write
|
|
16
16
|
id-token: write
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
19
19
|
with:
|
|
20
20
|
persist-credentials: false
|
|
21
|
-
- uses: ossf/scorecard-action@
|
|
21
|
+
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
|
22
22
|
with:
|
|
23
23
|
results_file: results.sarif
|
|
24
24
|
results_format: sarif
|
|
@@ -28,6 +28,6 @@ jobs:
|
|
|
28
28
|
name: scorecard-results
|
|
29
29
|
path: results.sarif
|
|
30
30
|
retention-days: 5
|
|
31
|
-
- uses: github/codeql-action/upload-sarif@
|
|
31
|
+
- uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
|
32
32
|
with:
|
|
33
33
|
sarif_file: results.sarif
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: zipwire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Read and extract files from remote ZIP archives over HTTP range requests
|
|
5
5
|
Project-URL: Homepage, https://github.com/tiran/zipwire
|
|
6
6
|
Project-URL: Documentation, https://zipwire.readthedocs.io/
|
|
@@ -60,8 +60,13 @@ CDNs, object stores, and static file servers do.
|
|
|
60
60
|
memory usage low even for large entries.
|
|
61
61
|
- **Sync and async** - `SyncRemoteZip` for synchronous code,
|
|
62
62
|
`AsyncRemoteZip` with `await`/`async with` for asyncio.
|
|
63
|
+
- **Wheel metadata** - `SyncRemoteWheel` / `AsyncRemoteWheel` read a Python
|
|
64
|
+
wheel's `.dist-info` (METADATA, WHEEL, RECORD) straight from PyPI in a single
|
|
65
|
+
adaptive tail request, without downloading the wheel.
|
|
63
66
|
- **ZIP64** - supports archives and entries larger than 4 GiB.
|
|
64
67
|
- **Pluggable backends** - bring your own HTTP library (see below).
|
|
68
|
+
- **Local files** - `FileReader` / `AsyncFileReader` open an archive on disk
|
|
69
|
+
through the exact same API, no HTTP server required.
|
|
65
70
|
|
|
66
71
|
## Installation and backends
|
|
67
72
|
|
|
@@ -81,11 +86,36 @@ pip install zipwire[httpx2]
|
|
|
81
86
|
| requests | `RequestsReader` | sync | 1.1 | `requests` |
|
|
82
87
|
| aiohttp | `AiohttpReader` | async | 1.1 | `aiohttp` |
|
|
83
88
|
|
|
84
|
-
Every backend accepts an optional pre-configured client or session so you
|
|
85
|
-
share connection pools, authentication, and retry configuration.
|
|
89
|
+
Every HTTP backend accepts an optional pre-configured client or session so you
|
|
90
|
+
can share connection pools, authentication, and retry configuration.
|
|
91
|
+
|
|
92
|
+
For archives that already live on the local filesystem, `FileReader` and
|
|
93
|
+
`AsyncFileReader` (in `zipwire.backends`, no extra dependency) satisfy the same
|
|
94
|
+
reader protocols - see the local-file example below.
|
|
86
95
|
|
|
87
96
|
## Examples
|
|
88
97
|
|
|
98
|
+
### Read Python wheel metadata without downloading the wheel
|
|
99
|
+
|
|
100
|
+
A common use case: fetch a wheel's `METADATA`, `WHEEL`, or `RECORD` from PyPI
|
|
101
|
+
without downloading the (often huge) wheel itself. `SyncRemoteWheel` and
|
|
102
|
+
`AsyncRemoteWheel` parse the wheel URL to locate the `.dist-info` directory and
|
|
103
|
+
fetch an adaptive tail, so metadata entries are served from memory without
|
|
104
|
+
extra HTTP requests:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from zipwire import SyncRemoteWheel
|
|
108
|
+
from zipwire.backends import Urllib3Reader
|
|
109
|
+
|
|
110
|
+
url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl"
|
|
111
|
+
with SyncRemoteWheel(Urllib3Reader(url)) as whl:
|
|
112
|
+
print(whl.read(whl.metadata_name).decode())
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This optimization relies on the [recommended wheel layout](https://packaging.python.org/en/latest/specifications/binary-distribution-format/#recommended-archiver-features)
|
|
116
|
+
of placing `.dist-info` at the end of the archive; wheels built otherwise still
|
|
117
|
+
work, falling back to a normal range request per entry.
|
|
118
|
+
|
|
89
119
|
### Sync - list files and read one
|
|
90
120
|
|
|
91
121
|
```python
|
|
@@ -130,6 +160,21 @@ async def main():
|
|
|
130
160
|
asyncio.run(main())
|
|
131
161
|
```
|
|
132
162
|
|
|
163
|
+
### Local archive - same API, no HTTP
|
|
164
|
+
|
|
165
|
+
`FileReader` opens an archive from disk through the same interface, accepting a
|
|
166
|
+
path or a `file://` URI. `AsyncFileReader` is the async counterpart. This lets
|
|
167
|
+
code that already uses zipwire handle local and remote archives the same way,
|
|
168
|
+
without special-casing either.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from zipwire import SyncRemoteZip
|
|
172
|
+
from zipwire.backends import FileReader
|
|
173
|
+
|
|
174
|
+
with SyncRemoteZip(FileReader("/path/to/archive.zip")) as rz:
|
|
175
|
+
data = rz.read("path/to/file.txt")
|
|
176
|
+
```
|
|
177
|
+
|
|
133
178
|
## License
|
|
134
179
|
|
|
135
180
|
Apache-2.0
|
|
@@ -25,8 +25,13 @@ CDNs, object stores, and static file servers do.
|
|
|
25
25
|
memory usage low even for large entries.
|
|
26
26
|
- **Sync and async** - `SyncRemoteZip` for synchronous code,
|
|
27
27
|
`AsyncRemoteZip` with `await`/`async with` for asyncio.
|
|
28
|
+
- **Wheel metadata** - `SyncRemoteWheel` / `AsyncRemoteWheel` read a Python
|
|
29
|
+
wheel's `.dist-info` (METADATA, WHEEL, RECORD) straight from PyPI in a single
|
|
30
|
+
adaptive tail request, without downloading the wheel.
|
|
28
31
|
- **ZIP64** - supports archives and entries larger than 4 GiB.
|
|
29
32
|
- **Pluggable backends** - bring your own HTTP library (see below).
|
|
33
|
+
- **Local files** - `FileReader` / `AsyncFileReader` open an archive on disk
|
|
34
|
+
through the exact same API, no HTTP server required.
|
|
30
35
|
|
|
31
36
|
## Installation and backends
|
|
32
37
|
|
|
@@ -46,11 +51,36 @@ pip install zipwire[httpx2]
|
|
|
46
51
|
| requests | `RequestsReader` | sync | 1.1 | `requests` |
|
|
47
52
|
| aiohttp | `AiohttpReader` | async | 1.1 | `aiohttp` |
|
|
48
53
|
|
|
49
|
-
Every backend accepts an optional pre-configured client or session so you
|
|
50
|
-
share connection pools, authentication, and retry configuration.
|
|
54
|
+
Every HTTP backend accepts an optional pre-configured client or session so you
|
|
55
|
+
can share connection pools, authentication, and retry configuration.
|
|
56
|
+
|
|
57
|
+
For archives that already live on the local filesystem, `FileReader` and
|
|
58
|
+
`AsyncFileReader` (in `zipwire.backends`, no extra dependency) satisfy the same
|
|
59
|
+
reader protocols - see the local-file example below.
|
|
51
60
|
|
|
52
61
|
## Examples
|
|
53
62
|
|
|
63
|
+
### Read Python wheel metadata without downloading the wheel
|
|
64
|
+
|
|
65
|
+
A common use case: fetch a wheel's `METADATA`, `WHEEL`, or `RECORD` from PyPI
|
|
66
|
+
without downloading the (often huge) wheel itself. `SyncRemoteWheel` and
|
|
67
|
+
`AsyncRemoteWheel` parse the wheel URL to locate the `.dist-info` directory and
|
|
68
|
+
fetch an adaptive tail, so metadata entries are served from memory without
|
|
69
|
+
extra HTTP requests:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from zipwire import SyncRemoteWheel
|
|
73
|
+
from zipwire.backends import Urllib3Reader
|
|
74
|
+
|
|
75
|
+
url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl"
|
|
76
|
+
with SyncRemoteWheel(Urllib3Reader(url)) as whl:
|
|
77
|
+
print(whl.read(whl.metadata_name).decode())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This optimization relies on the [recommended wheel layout](https://packaging.python.org/en/latest/specifications/binary-distribution-format/#recommended-archiver-features)
|
|
81
|
+
of placing `.dist-info` at the end of the archive; wheels built otherwise still
|
|
82
|
+
work, falling back to a normal range request per entry.
|
|
83
|
+
|
|
54
84
|
### Sync - list files and read one
|
|
55
85
|
|
|
56
86
|
```python
|
|
@@ -95,6 +125,21 @@ async def main():
|
|
|
95
125
|
asyncio.run(main())
|
|
96
126
|
```
|
|
97
127
|
|
|
128
|
+
### Local archive - same API, no HTTP
|
|
129
|
+
|
|
130
|
+
`FileReader` opens an archive from disk through the same interface, accepting a
|
|
131
|
+
path or a `file://` URI. `AsyncFileReader` is the async counterpart. This lets
|
|
132
|
+
code that already uses zipwire handle local and remote archives the same way,
|
|
133
|
+
without special-casing either.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from zipwire import SyncRemoteZip
|
|
137
|
+
from zipwire.backends import FileReader
|
|
138
|
+
|
|
139
|
+
with SyncRemoteZip(FileReader("/path/to/archive.zip")) as rz:
|
|
140
|
+
data = rz.read("path/to/file.txt")
|
|
141
|
+
```
|
|
142
|
+
|
|
98
143
|
## License
|
|
99
144
|
|
|
100
145
|
Apache-2.0
|
|
@@ -18,12 +18,27 @@ Core
|
|
|
18
18
|
|
|
19
19
|
.. autoclass:: zipwire.SyncRemoteZip
|
|
20
20
|
:members:
|
|
21
|
+
:inherited-members:
|
|
21
22
|
:special-members: __enter__, __exit__
|
|
22
23
|
|
|
23
24
|
.. autoclass:: zipwire.AsyncRemoteZip
|
|
24
25
|
:members:
|
|
26
|
+
:inherited-members:
|
|
25
27
|
:special-members: __aenter__, __aexit__
|
|
26
28
|
|
|
29
|
+
Wheel
|
|
30
|
+
-----
|
|
31
|
+
|
|
32
|
+
.. autoclass:: zipwire.SyncRemoteWheel
|
|
33
|
+
:members:
|
|
34
|
+
:inherited-members:
|
|
35
|
+
:show-inheritance:
|
|
36
|
+
|
|
37
|
+
.. autoclass:: zipwire.AsyncRemoteWheel
|
|
38
|
+
:members:
|
|
39
|
+
:inherited-members:
|
|
40
|
+
:show-inheritance:
|
|
41
|
+
|
|
27
42
|
.. autoclass:: zipwire.RemoteZipInfo
|
|
28
43
|
:show-inheritance:
|
|
29
44
|
|
|
@@ -63,6 +78,14 @@ Backends
|
|
|
63
78
|
.. autoclass:: zipwire.backends._aiohttp.AiohttpReader
|
|
64
79
|
:members:
|
|
65
80
|
|
|
81
|
+
.. autoclass:: zipwire.backends._file.FileReader
|
|
82
|
+
:members:
|
|
83
|
+
:special-members: __enter__, __exit__
|
|
84
|
+
|
|
85
|
+
.. autoclass:: zipwire.backends._file.AsyncFileReader
|
|
86
|
+
:members:
|
|
87
|
+
:special-members: __aenter__, __aexit__
|
|
88
|
+
|
|
66
89
|
Exceptions
|
|
67
90
|
----------
|
|
68
91
|
|
|
@@ -43,6 +43,16 @@ Available backends
|
|
|
43
43
|
- async
|
|
44
44
|
- 1.1
|
|
45
45
|
- ``aiohttp``
|
|
46
|
+
* - local files
|
|
47
|
+
- :class:`~zipwire.backends.FileReader`
|
|
48
|
+
- sync
|
|
49
|
+
- --
|
|
50
|
+
- *(included)*
|
|
51
|
+
* - local files
|
|
52
|
+
- :class:`~zipwire.backends.AsyncFileReader`
|
|
53
|
+
- async
|
|
54
|
+
- --
|
|
55
|
+
- *(included)*
|
|
46
56
|
|
|
47
57
|
Choosing a backend
|
|
48
58
|
------------------
|
|
@@ -60,6 +70,57 @@ Choosing a backend
|
|
|
60
70
|
- **Requests integration** - use :class:`~zipwire.backends.RequestsReader`
|
|
61
71
|
if your project already uses ``requests`` and you want to share sessions,
|
|
62
72
|
authentication, or retry configuration.
|
|
73
|
+
- **Local archives** - use :class:`~zipwire.backends.FileReader` or
|
|
74
|
+
:class:`~zipwire.backends.AsyncFileReader` to read a ZIP or wheel that
|
|
75
|
+
already lives on the local filesystem. No extra dependency is required.
|
|
76
|
+
|
|
77
|
+
Reading local files
|
|
78
|
+
-------------------
|
|
79
|
+
|
|
80
|
+
:class:`~zipwire.backends.FileReader` and
|
|
81
|
+
:class:`~zipwire.backends.AsyncFileReader` back the same
|
|
82
|
+
:class:`~zipwire.SyncRemoteZip` / :class:`~zipwire.AsyncRemoteZip` API with
|
|
83
|
+
local file IO, so identical code opens a local or a remote archive. Instead
|
|
84
|
+
of an HTTP round-trip, :meth:`~zipwire.backends.FileReader.head` synthesises
|
|
85
|
+
``Content-Length`` / ``Accept-Ranges`` from ``os.fstat`` on the open handle
|
|
86
|
+
(the size is cached, assuming the file does not change), and range reads
|
|
87
|
+
seek into a lazily-opened file handle. A missing path raises
|
|
88
|
+
:exc:`FileNotFoundError`, mirroring how a network 404 surfaces as
|
|
89
|
+
:exc:`OSError`.
|
|
90
|
+
|
|
91
|
+
Both accept either a path (``str`` / :class:`os.PathLike`) or a ``file://``
|
|
92
|
+
URI via :meth:`~zipwire.backends.FileReader.from_uri` (percent-decoded, with
|
|
93
|
+
an empty or ``localhost`` host).
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
from zipwire import SyncRemoteZip
|
|
98
|
+
from zipwire.backends import FileReader
|
|
99
|
+
|
|
100
|
+
with SyncRemoteZip(FileReader("/path/to/archive.zip")) as rz:
|
|
101
|
+
data = rz.read("file.txt")
|
|
102
|
+
|
|
103
|
+
reader = FileReader.from_uri("file:///path/to/archive.zip")
|
|
104
|
+
|
|
105
|
+
Regular files do not support true non-blocking IO, so
|
|
106
|
+
:class:`~zipwire.backends.AsyncFileReader` offloads every blocking call to a
|
|
107
|
+
worker thread via :func:`asyncio.to_thread` -- the same approach libraries
|
|
108
|
+
such as ``aiofiles`` use internally, without the extra dependency.
|
|
109
|
+
|
|
110
|
+
.. code-block:: python
|
|
111
|
+
|
|
112
|
+
import asyncio
|
|
113
|
+
|
|
114
|
+
from zipwire import AsyncRemoteZip
|
|
115
|
+
from zipwire.backends import AsyncFileReader
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def main():
|
|
119
|
+
async with AsyncRemoteZip(AsyncFileReader("/path/to/archive.zip")) as rz:
|
|
120
|
+
print(await rz.read("file.txt"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
63
124
|
|
|
64
125
|
Passing an existing client
|
|
65
126
|
--------------------------
|
|
@@ -46,6 +46,25 @@ entries:
|
|
|
46
46
|
with open("output.bin", "wb") as f:
|
|
47
47
|
rz.read_into("big-file.bin", f)
|
|
48
48
|
|
|
49
|
+
Read a local archive
|
|
50
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
51
|
+
|
|
52
|
+
:class:`~zipwire.backends.FileReader` opens an archive on the local
|
|
53
|
+
filesystem through the same interface, so the same code works for local and
|
|
54
|
+
remote archives. It accepts a path or a ``file://`` URI and needs no extra
|
|
55
|
+
dependency:
|
|
56
|
+
|
|
57
|
+
.. code-block:: python
|
|
58
|
+
|
|
59
|
+
from zipwire import SyncRemoteZip
|
|
60
|
+
from zipwire.backends import FileReader
|
|
61
|
+
|
|
62
|
+
with SyncRemoteZip(FileReader("/path/to/archive.zip")) as rz:
|
|
63
|
+
data = rz.read("path/to/file.txt")
|
|
64
|
+
|
|
65
|
+
# Or from a file:// URI
|
|
66
|
+
reader = FileReader.from_uri("file:///path/to/archive.zip")
|
|
67
|
+
|
|
49
68
|
Asynchronous usage
|
|
50
69
|
------------------
|
|
51
70
|
|
|
@@ -83,12 +102,33 @@ Async with httpx2
|
|
|
83
102
|
async def main():
|
|
84
103
|
reader = Httpx2AsyncReader("https://archive.example/data.zip")
|
|
85
104
|
async with AsyncRemoteZip(reader) as rz:
|
|
86
|
-
names =
|
|
105
|
+
names = rz.namelist()
|
|
87
106
|
print(names)
|
|
88
107
|
|
|
89
108
|
|
|
90
109
|
asyncio.run(main())
|
|
91
110
|
|
|
111
|
+
Async local archive
|
|
112
|
+
^^^^^^^^^^^^^^^^^^^
|
|
113
|
+
|
|
114
|
+
:class:`~zipwire.backends.AsyncFileReader` reads a local archive and offloads
|
|
115
|
+
blocking file IO to a worker thread so it never stalls the event loop:
|
|
116
|
+
|
|
117
|
+
.. code-block:: python
|
|
118
|
+
|
|
119
|
+
import asyncio
|
|
120
|
+
|
|
121
|
+
from zipwire import AsyncRemoteZip
|
|
122
|
+
from zipwire.backends import AsyncFileReader
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def main():
|
|
126
|
+
async with AsyncRemoteZip(AsyncFileReader("/path/to/archive.zip")) as rz:
|
|
127
|
+
print(await rz.read("path/to/file.txt"))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
asyncio.run(main())
|
|
131
|
+
|
|
92
132
|
Async streaming to disk
|
|
93
133
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
94
134
|
|
|
@@ -109,6 +149,47 @@ Async streaming to disk
|
|
|
109
149
|
|
|
110
150
|
asyncio.run(main())
|
|
111
151
|
|
|
152
|
+
Wheel dist-info metadata
|
|
153
|
+
------------------------
|
|
154
|
+
|
|
155
|
+
``SyncRemoteWheel`` and ``AsyncRemoteWheel`` extend the base classes
|
|
156
|
+
with an adaptive tail fetch and a ``distinfolist()`` method. The tail
|
|
157
|
+
size scales with the archive (at least 128 KiB, up to ~0.4% of the
|
|
158
|
+
file) so that dist-info entries are typically served from memory
|
|
159
|
+
without extra HTTP requests.
|
|
160
|
+
|
|
161
|
+
.. code-block:: python
|
|
162
|
+
|
|
163
|
+
from zipwire import SyncRemoteWheel
|
|
164
|
+
from zipwire.backends import Urllib3Reader
|
|
165
|
+
|
|
166
|
+
url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl"
|
|
167
|
+
reader = Urllib3Reader(url)
|
|
168
|
+
with SyncRemoteWheel(reader) as whl:
|
|
169
|
+
for entry in whl.distinfolist():
|
|
170
|
+
data = whl.read(entry)
|
|
171
|
+
print(entry.filename, len(data))
|
|
172
|
+
|
|
173
|
+
Async variant:
|
|
174
|
+
|
|
175
|
+
.. code-block:: python
|
|
176
|
+
|
|
177
|
+
import asyncio
|
|
178
|
+
|
|
179
|
+
from zipwire import AsyncRemoteWheel
|
|
180
|
+
from zipwire.backends import AiohttpReader
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def main():
|
|
184
|
+
reader = AiohttpReader(url)
|
|
185
|
+
async with AsyncRemoteWheel(reader) as whl:
|
|
186
|
+
for entry in whl.distinfolist():
|
|
187
|
+
data = await whl.read(entry)
|
|
188
|
+
print(entry.filename, len(data))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
asyncio.run(main())
|
|
192
|
+
|
|
112
193
|
Passing a pre-configured client
|
|
113
194
|
--------------------------------
|
|
114
195
|
|
|
@@ -34,7 +34,7 @@ Asynchronous example
|
|
|
34
34
|
async def main():
|
|
35
35
|
reader = AiohttpReader("https://example.com/archive.zip")
|
|
36
36
|
async with AsyncRemoteZip(reader) as rz:
|
|
37
|
-
print(
|
|
37
|
+
print(rz.namelist())
|
|
38
38
|
data = await rz.read("path/inside/archive.txt")
|
|
39
39
|
|
|
40
40
|
# Stream a large entry directly to disk
|
|
@@ -58,6 +58,44 @@ Asynchronous:
|
|
|
58
58
|
- ``AiohttpReader`` -- uses *aiohttp* (``pip install zipwire[aiohttp]``)
|
|
59
59
|
- ``Httpx2AsyncReader`` -- uses *httpx2*, supports HTTP/2
|
|
60
60
|
(``pip install zipwire[httpx2]``)
|
|
61
|
+
|
|
62
|
+
Local files:
|
|
63
|
+
- ``FileReader`` / ``AsyncFileReader`` -- read an archive from the local
|
|
64
|
+
filesystem through the same API (no extra dependency). Useful for
|
|
65
|
+
testing or for handling ``file://`` URIs and local paths uniformly
|
|
66
|
+
with HTTP URLs.
|
|
67
|
+
|
|
68
|
+
::
|
|
69
|
+
|
|
70
|
+
from zipwire import SyncRemoteZip
|
|
71
|
+
from zipwire.backends import FileReader
|
|
72
|
+
|
|
73
|
+
with SyncRemoteZip(FileReader("/path/to/archive.zip")) as rz:
|
|
74
|
+
data = rz.read("path/inside/archive.txt")
|
|
75
|
+
|
|
76
|
+
# From a file:// URI
|
|
77
|
+
reader = FileReader.from_uri("file:///path/to/archive.zip")
|
|
78
|
+
|
|
79
|
+
Wheel subclasses
|
|
80
|
+
----------------
|
|
81
|
+
|
|
82
|
+
``SyncRemoteWheel`` and ``AsyncRemoteWheel`` extend the base classes
|
|
83
|
+
for Python wheels. They parse the wheel URL to derive the
|
|
84
|
+
``.dist-info`` directory name, fetch an adaptive tail (at least
|
|
85
|
+
128 KiB, scaling with archive size) to cover dist-info entries at
|
|
86
|
+
the end of the archive, and serve matching reads from the tail
|
|
87
|
+
buffer without extra HTTP requests.
|
|
88
|
+
|
|
89
|
+
::
|
|
90
|
+
|
|
91
|
+
from zipwire import SyncRemoteWheel
|
|
92
|
+
from zipwire.backends import Urllib3Reader
|
|
93
|
+
|
|
94
|
+
url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl"
|
|
95
|
+
reader = Urllib3Reader(url)
|
|
96
|
+
with SyncRemoteWheel(reader) as whl:
|
|
97
|
+
for entry in whl.distinfolist():
|
|
98
|
+
print(entry.filename, whl.read(entry))
|
|
61
99
|
"""
|
|
62
100
|
|
|
63
101
|
from __future__ import annotations
|
|
@@ -76,10 +114,12 @@ from zipwire._errors import (
|
|
|
76
114
|
from zipwire._parser import EOCDInfo
|
|
77
115
|
from zipwire._sync import SyncRemoteZip
|
|
78
116
|
from zipwire._types import AsyncReader, Headers, SyncReader, Writable
|
|
117
|
+
from zipwire._wheel import AsyncRemoteWheel, SyncRemoteWheel
|
|
79
118
|
from zipwire._zipinfo import RemoteZipInfo
|
|
80
119
|
|
|
81
120
|
__all__ = [
|
|
82
121
|
"AsyncReader",
|
|
122
|
+
"AsyncRemoteWheel",
|
|
83
123
|
"AsyncRemoteZip",
|
|
84
124
|
"BadZipFile",
|
|
85
125
|
"CRCMismatch",
|
|
@@ -91,6 +131,7 @@ __all__ = [
|
|
|
91
131
|
"RangeRequestUnsupported",
|
|
92
132
|
"RemoteZipInfo",
|
|
93
133
|
"SyncReader",
|
|
134
|
+
"SyncRemoteWheel",
|
|
94
135
|
"SyncRemoteZip",
|
|
95
136
|
"UnsupportedCompression",
|
|
96
137
|
"Writable",
|
|
@@ -41,7 +41,7 @@ async def _run_async(url: str, backend: str, skip_dirs: bool) -> None:
|
|
|
41
41
|
names = {"httpx2": "Httpx2AsyncReader", "aiohttp": "AiohttpReader"}
|
|
42
42
|
reader_cls = getattr(backends, names[backend])
|
|
43
43
|
async with AsyncRemoteZip(reader_cls(url)) as rz:
|
|
44
|
-
_print_table(
|
|
44
|
+
_print_table(rz.infolist(), skip_dirs)
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def main(argv: list[str] | None = None) -> None:
|