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.
Files changed (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. 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