nab-python 0.0.1__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.
- nab_python/__init__.py +1 -0
- nab_python/_build/__init__.py +1 -0
- nab_python/_build/env.py +364 -0
- nab_python/_build/errors.py +17 -0
- nab_python/_build/runner.py +254 -0
- nab_python/_lockfile/__init__.py +1 -0
- nab_python/_lockfile/builder.py +339 -0
- nab_python/_lockfile/disjointness.py +207 -0
- nab_python/_lockfile/pylock.py +323 -0
- nab_python/_lockfile/requirements.py +121 -0
- nab_python/_packaging_provider.py +98 -0
- nab_python/_provider/__init__.py +1 -0
- nab_python/_provider/build_remote.py +95 -0
- nab_python/_provider/extras.py +231 -0
- nab_python/_provider/listing.py +442 -0
- nab_python/_provider/lookahead.py +156 -0
- nab_python/_provider/metadata_resolver.py +450 -0
- nab_python/_provider/priority.py +174 -0
- nab_python/_provider/sources.py +215 -0
- nab_python/_testing/__init__.py +1 -0
- nab_python/_testing/coordinator_fake.py +240 -0
- nab_python/_vcs_admission.py +209 -0
- nab_python/_vendor/__init__.py +6 -0
- nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python/_vendor/packaging/errors.py +94 -0
- nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python/_vendor/packaging/markers.py +506 -0
- nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python/_vendor/packaging/py.typed +0 -0
- nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python/_vendor/packaging/ranges.py +1803 -0
- nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python/_vendor/packaging/specifiers.py +1141 -0
- nab_python/_vendor/packaging/tags.py +929 -0
- nab_python/_vendor/packaging/utils.py +296 -0
- nab_python/_vendor/packaging/version.py +1230 -0
- nab_python/build_backend.py +184 -0
- nab_python/config.py +805 -0
- nab_python/download.py +170 -0
- nab_python/fetch.py +827 -0
- nab_python/lockfile.py +238 -0
- nab_python/metadata.py +145 -0
- nab_python/provider.py +1235 -0
- nab_python/py.typed +0 -0
- nab_python/requirements_file.py +180 -0
- nab_python/resolve.py +497 -0
- nab_python/universal/__init__.py +1 -0
- nab_python/universal/matrix.py +235 -0
- nab_python/universal/provider.py +214 -0
- nab_python/universal/reresolve.py +310 -0
- nab_python/universal/resolve.py +508 -0
- nab_python/universal/validate.py +439 -0
- nab_python/universal/wheel_selection.py +327 -0
- nab_python/workspace.py +214 -0
- nab_python-0.0.1.dist-info/METADATA +49 -0
- nab_python-0.0.1.dist-info/RECORD +71 -0
- nab_python-0.0.1.dist-info/WHEEL +4 -0
- nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
nab_python/download.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Download every distribution referenced by a finished resolve.
|
|
2
|
+
|
|
3
|
+
Consumes a :class:`~nab_python.lockfile.LockInput` and writes every
|
|
4
|
+
recorded wheel and sdist into a target directory, verifying the
|
|
5
|
+
recorded hash. Local and VCS pins are skipped: their contents live
|
|
6
|
+
elsewhere on disk and the lockfile carries no ``sha256`` for them.
|
|
7
|
+
|
|
8
|
+
Use as a one-shot from the CLI ``nab download`` command, or
|
|
9
|
+
programmatically after :func:`~nab_python.resolve.resolve_pyproject_to_lock`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from nab_index.client import AsyncSimpleClient
|
|
21
|
+
|
|
22
|
+
from .lockfile import IndexPin, LocalPin, LockInput, VcsPin
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from nab_index.transport import AsyncHttpTransport
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"DownloadEntry",
|
|
32
|
+
"DownloadError",
|
|
33
|
+
"DownloadResult",
|
|
34
|
+
"download_lock",
|
|
35
|
+
"iter_artifacts",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DownloadError(Exception):
|
|
43
|
+
"""A downloaded artefact failed hash verification or the HTTP fetch."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class DownloadEntry:
|
|
48
|
+
"""One artefact to fetch into the output directory.
|
|
49
|
+
|
|
50
|
+
``hash_algo`` is one of ``sha256``, ``sha384``, ``sha512`` and
|
|
51
|
+
``digest`` is the recorded hex digest under that algorithm.
|
|
52
|
+
The downloader picks the strongest acceptable algorithm the
|
|
53
|
+
index published; it does not re-hash with a weaker one.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
package: str
|
|
57
|
+
version: str
|
|
58
|
+
filename: str
|
|
59
|
+
url: str
|
|
60
|
+
hash_algo: str
|
|
61
|
+
digest: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class DownloadResult:
|
|
66
|
+
"""Summary of a download run."""
|
|
67
|
+
|
|
68
|
+
written: tuple[Path, ...]
|
|
69
|
+
skipped: tuple[Path, ...]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def iter_artifacts(lock_input: LockInput) -> Iterable[DownloadEntry]:
|
|
73
|
+
"""Yield every wheel/sdist artefact referenced by ``lock_input``."""
|
|
74
|
+
for canonical, pin in sorted(lock_input.pins.items()):
|
|
75
|
+
if isinstance(pin, IndexPin):
|
|
76
|
+
yield from _iter_index_pin(canonical, pin)
|
|
77
|
+
elif isinstance(pin, (LocalPin, VcsPin)):
|
|
78
|
+
continue
|
|
79
|
+
else: # pragma: no cover - exhaustive
|
|
80
|
+
msg = f"unknown pin shape: {pin!r}"
|
|
81
|
+
raise TypeError(msg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
85
|
+
if pin.sdist is not None:
|
|
86
|
+
algo, digest = pin.sdist.primary_digest
|
|
87
|
+
yield DownloadEntry(
|
|
88
|
+
package=canonical,
|
|
89
|
+
version=pin.version,
|
|
90
|
+
filename=pin.sdist.filename,
|
|
91
|
+
url=pin.sdist.url,
|
|
92
|
+
hash_algo=algo,
|
|
93
|
+
digest=digest,
|
|
94
|
+
)
|
|
95
|
+
for wheel in pin.wheels:
|
|
96
|
+
algo, digest = wheel.primary_digest
|
|
97
|
+
yield DownloadEntry(
|
|
98
|
+
package=canonical,
|
|
99
|
+
version=pin.version,
|
|
100
|
+
filename=wheel.filename,
|
|
101
|
+
url=wheel.url,
|
|
102
|
+
hash_algo=algo,
|
|
103
|
+
digest=digest,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def download_lock(
|
|
108
|
+
lock_input: LockInput,
|
|
109
|
+
transport: AsyncHttpTransport,
|
|
110
|
+
output_dir: Path,
|
|
111
|
+
*,
|
|
112
|
+
max_concurrency: int = 8,
|
|
113
|
+
) -> DownloadResult:
|
|
114
|
+
"""Download every artefact in ``lock_input`` into ``output_dir``.
|
|
115
|
+
|
|
116
|
+
Already-present files whose digest matches the recorded
|
|
117
|
+
algorithm are left alone so the command is idempotent.
|
|
118
|
+
Mismatched files are re-fetched and overwritten. HTTP failures
|
|
119
|
+
and post-download hash mismatches both raise
|
|
120
|
+
:class:`DownloadError` after the fetcher has shut down.
|
|
121
|
+
"""
|
|
122
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
artefacts = list(iter_artifacts(lock_input))
|
|
124
|
+
return asyncio.run(
|
|
125
|
+
_run_downloads(artefacts, transport, output_dir, max_concurrency)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _run_downloads(
|
|
130
|
+
artefacts: list[DownloadEntry],
|
|
131
|
+
transport: AsyncHttpTransport,
|
|
132
|
+
output_dir: Path,
|
|
133
|
+
max_concurrency: int,
|
|
134
|
+
) -> DownloadResult:
|
|
135
|
+
sem = asyncio.Semaphore(max_concurrency)
|
|
136
|
+
client = AsyncSimpleClient(transport)
|
|
137
|
+
written: list[Path] = []
|
|
138
|
+
skipped: list[Path] = []
|
|
139
|
+
try:
|
|
140
|
+
|
|
141
|
+
async def _one(entry: DownloadEntry) -> None:
|
|
142
|
+
async with sem:
|
|
143
|
+
target = output_dir / entry.filename
|
|
144
|
+
if _already_present(target, entry.hash_algo, entry.digest):
|
|
145
|
+
skipped.append(target)
|
|
146
|
+
logger.info("skip %s (%s matches)", entry.filename, entry.hash_algo)
|
|
147
|
+
return
|
|
148
|
+
data = await client.download(entry.url)
|
|
149
|
+
actual = hashlib.new(entry.hash_algo, data).hexdigest()
|
|
150
|
+
if actual != entry.digest:
|
|
151
|
+
msg = (
|
|
152
|
+
f"{entry.package}=={entry.version}: {entry.hash_algo}"
|
|
153
|
+
f" mismatch for {entry.filename}\n"
|
|
154
|
+
f" expected: {entry.digest}\n actual: {actual}"
|
|
155
|
+
)
|
|
156
|
+
raise DownloadError(msg)
|
|
157
|
+
target.write_bytes(data)
|
|
158
|
+
written.append(target)
|
|
159
|
+
logger.info("wrote %s", target)
|
|
160
|
+
|
|
161
|
+
await asyncio.gather(*(_one(a) for a in artefacts))
|
|
162
|
+
finally:
|
|
163
|
+
await client.aclose()
|
|
164
|
+
return DownloadResult(written=tuple(written), skipped=tuple(skipped))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _already_present(target: Path, algo: str, expected_digest: str) -> bool:
|
|
168
|
+
if not target.exists():
|
|
169
|
+
return False
|
|
170
|
+
return hashlib.new(algo, target.read_bytes()).hexdigest() == expected_digest
|