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/lockfile.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""PEP 751 lockfile (``pylock.toml``) emission for nab-python.
|
|
2
|
+
|
|
3
|
+
Public surface for producing lockfiles from a resolve. The three
|
|
4
|
+
emitters are :func:`write_lock` (PEP 751 ``pylock.toml``),
|
|
5
|
+
:func:`write_requirements_with_hashes`, and
|
|
6
|
+
:func:`write_requirements_without_hashes`. Per-tuple pins from a
|
|
7
|
+
universal resolve are collapsed into one ``Package`` per distinct
|
|
8
|
+
``(name, version, source)`` with a marker disjoining the matching
|
|
9
|
+
tuples; same-version tuples emit a single entry with no marker.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from ._lockfile.builder import (
|
|
19
|
+
MissingHashError,
|
|
20
|
+
build_lock_input_from_provider,
|
|
21
|
+
read_lockfile_anchor,
|
|
22
|
+
)
|
|
23
|
+
from ._lockfile.disjointness import DisjointnessError
|
|
24
|
+
from ._lockfile.pylock import build_pylock, write_lock
|
|
25
|
+
from ._lockfile.requirements import (
|
|
26
|
+
write_requirements_with_hashes,
|
|
27
|
+
write_requirements_without_hashes,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
|
|
34
|
+
from ._vendor.packaging.markers import Marker
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ACCEPTED_HASH_ALGORITHMS",
|
|
39
|
+
"LOCK_VERSION",
|
|
40
|
+
"DisjointnessError",
|
|
41
|
+
"IndexPin",
|
|
42
|
+
"LocalPin",
|
|
43
|
+
"LockInput",
|
|
44
|
+
"MissingHashError",
|
|
45
|
+
"PinShape",
|
|
46
|
+
"Provenance",
|
|
47
|
+
"SdistArtifact",
|
|
48
|
+
"VcsPin",
|
|
49
|
+
"WheelArtifact",
|
|
50
|
+
"build_lock_input_from_provider",
|
|
51
|
+
"build_pylock",
|
|
52
|
+
"read_lockfile_anchor",
|
|
53
|
+
"write_lock",
|
|
54
|
+
"write_requirements_with_hashes",
|
|
55
|
+
"write_requirements_without_hashes",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
LOCK_VERSION = "1.0"
|
|
62
|
+
ACCEPTED_HASH_ALGORITHMS: tuple[str, ...] = ("sha256", "sha384", "sha512")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _select_primary_digest(
|
|
66
|
+
hashes: tuple[tuple[str, str], ...],
|
|
67
|
+
) -> tuple[str, str] | None:
|
|
68
|
+
by_algo = dict(hashes)
|
|
69
|
+
for algo in ACCEPTED_HASH_ALGORITHMS:
|
|
70
|
+
if algo in by_algo:
|
|
71
|
+
return algo, by_algo[algo]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class WheelArtifact:
|
|
77
|
+
"""A single wheel file to record in the lockfile.
|
|
78
|
+
|
|
79
|
+
``hashes`` is the set of (algorithm, digest) pairs the index
|
|
80
|
+
published. PEP 751 mandates at least one hash per artefact;
|
|
81
|
+
nab requires at least one of ``sha256``, ``sha384``, ``sha512``
|
|
82
|
+
so the lockfile is consumable by pip's hash-checking mode.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
filename: str
|
|
86
|
+
url: str
|
|
87
|
+
hashes: tuple[tuple[str, str], ...]
|
|
88
|
+
size: int | None = None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def primary_digest(self) -> tuple[str, str]:
|
|
92
|
+
"""Return ``(algo, digest)`` of the strongest acceptable hash."""
|
|
93
|
+
chosen = _select_primary_digest(self.hashes)
|
|
94
|
+
if chosen is None:
|
|
95
|
+
msg = f"{self.filename} has no acceptable hash"
|
|
96
|
+
raise ValueError(msg)
|
|
97
|
+
return chosen
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class SdistArtifact:
|
|
102
|
+
"""An sdist tarball to record in the lockfile.
|
|
103
|
+
|
|
104
|
+
See :class:`WheelArtifact` for the meaning of ``hashes``.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
filename: str
|
|
108
|
+
url: str
|
|
109
|
+
hashes: tuple[tuple[str, str], ...]
|
|
110
|
+
size: int | None = None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def primary_digest(self) -> tuple[str, str]:
|
|
114
|
+
"""Return ``(algo, digest)`` of the strongest acceptable hash."""
|
|
115
|
+
chosen = _select_primary_digest(self.hashes)
|
|
116
|
+
if chosen is None:
|
|
117
|
+
msg = f"{self.filename} has no acceptable hash"
|
|
118
|
+
raise ValueError(msg)
|
|
119
|
+
return chosen
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class IndexPin:
|
|
124
|
+
"""A package resolved from a Simple-API index.
|
|
125
|
+
|
|
126
|
+
``index`` is the URL of the Simple-API root that served the
|
|
127
|
+
package, matching what PEP 751 expects for ``packages.index``.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
name: str
|
|
131
|
+
version: str
|
|
132
|
+
index: str
|
|
133
|
+
sdist: SdistArtifact | None = None
|
|
134
|
+
wheels: tuple[WheelArtifact, ...] = ()
|
|
135
|
+
requires_python: str | None = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class LocalPin:
|
|
140
|
+
"""A package resolved from a local checkout.
|
|
141
|
+
|
|
142
|
+
``path`` is the absolute filesystem path the resolver was pointed
|
|
143
|
+
at. Lockfile consumers walk the same tree to install.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
name: str
|
|
147
|
+
version: str
|
|
148
|
+
path: str
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class VcsPin:
|
|
153
|
+
"""A package resolved from a VCS clone."""
|
|
154
|
+
|
|
155
|
+
name: str
|
|
156
|
+
version: str
|
|
157
|
+
repo_url: str
|
|
158
|
+
commit_id: str
|
|
159
|
+
subdirectory: str | None = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
PinShape = IndexPin | LocalPin | VcsPin
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class Provenance:
|
|
167
|
+
"""Optional ``[tool.nab]`` provenance block written into the lock.
|
|
168
|
+
|
|
169
|
+
PEP 751 lets tools record any additional metadata under
|
|
170
|
+
``[tool.<name>]`` so long as it does not affect installation.
|
|
171
|
+
nab uses the slot to record the inputs that produced the lock:
|
|
172
|
+
a reader can audit a committed lockfile without re-running.
|
|
173
|
+
|
|
174
|
+
Every field is informational. The lockfile reader MUST NOT
|
|
175
|
+
feed any of it into the install path.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
nab_version: str
|
|
179
|
+
created_at: datetime
|
|
180
|
+
command_line: tuple[str, ...]
|
|
181
|
+
input_path: str
|
|
182
|
+
mode: str
|
|
183
|
+
python_specifier: str | None = None
|
|
184
|
+
platforms: tuple[str, ...] = ()
|
|
185
|
+
|
|
186
|
+
def to_block(self) -> dict[str, Any]:
|
|
187
|
+
"""Render to the dict the TOML writer drops under ``[tool.nab]``."""
|
|
188
|
+
block: dict[str, Any] = {
|
|
189
|
+
"nab-version": self.nab_version,
|
|
190
|
+
"created-at": self.created_at,
|
|
191
|
+
"command-line": list(self.command_line),
|
|
192
|
+
"input-path": self.input_path,
|
|
193
|
+
"mode": self.mode,
|
|
194
|
+
}
|
|
195
|
+
if self.python_specifier is not None:
|
|
196
|
+
block["python-specifier"] = self.python_specifier
|
|
197
|
+
if self.platforms:
|
|
198
|
+
block["platforms"] = list(self.platforms)
|
|
199
|
+
return block
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class LockInput:
|
|
204
|
+
"""Everything the writer needs to produce a Pylock.
|
|
205
|
+
|
|
206
|
+
``pins`` is keyed by canonical package name; each value is the
|
|
207
|
+
single pin chosen for that package across the resolve.
|
|
208
|
+
|
|
209
|
+
``per_tuple_pins`` is the per-tuple expansion when a universal
|
|
210
|
+
resolve produced different pins for different environments. The
|
|
211
|
+
writer collapses these into one or more ``Package`` entries with
|
|
212
|
+
markers attached. When ``per_tuple_pins`` is empty, ``pins`` is
|
|
213
|
+
used directly with no markers.
|
|
214
|
+
|
|
215
|
+
``tuple_markers`` maps each tuple label (the keys of
|
|
216
|
+
``per_tuple_pins``) to the PEP 508 marker that selects that
|
|
217
|
+
tuple. The writer uses these to build per-package markers.
|
|
218
|
+
|
|
219
|
+
``environments`` is the lockfile-level set of permitted
|
|
220
|
+
environments (PEP 751 ``environments``). Independent from
|
|
221
|
+
``tuple_markers``; intended for declaring the universe.
|
|
222
|
+
|
|
223
|
+
``provenance`` is optional metadata about the inputs that
|
|
224
|
+
produced this lock. When present, it lands in the ``[tool.nab]``
|
|
225
|
+
block of the emitted ``pylock.toml``.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
pins: Mapping[str, PinShape] = field(default_factory=dict)
|
|
229
|
+
per_tuple_pins: Mapping[str, Mapping[str, PinShape]] = field(default_factory=dict)
|
|
230
|
+
tuple_markers: Mapping[str, Marker] = field(default_factory=dict)
|
|
231
|
+
tuple_environments: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
|
|
232
|
+
environments: list[Marker] = field(default_factory=list)
|
|
233
|
+
requires_python: str | None = None
|
|
234
|
+
created_by: str = "nab"
|
|
235
|
+
extras: tuple[str, ...] = ()
|
|
236
|
+
dependency_groups: tuple[str, ...] = ()
|
|
237
|
+
default_groups: tuple[str, ...] = ()
|
|
238
|
+
provenance: Provenance | None = None
|
nab_python/metadata.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Minimal METADATA parser for nab-python.
|
|
2
|
+
|
|
3
|
+
Extracts only the fields needed for dependency resolution from PEP
|
|
4
|
+
566/643 METADATA files (RFC 822 format). Lighter than
|
|
5
|
+
:class:`packaging.metadata.Metadata` (no validation pass) and reuses
|
|
6
|
+
:class:`packaging.requirements.Requirement` parsing through an
|
|
7
|
+
LRU cache so repeated dep strings parse once.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import email.parser
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import tomli
|
|
18
|
+
|
|
19
|
+
from ._vendor.packaging.requirements import Requirement
|
|
20
|
+
from ._vendor.packaging.specifiers import SpecifierSet
|
|
21
|
+
from ._vendor.packaging.version import Version
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DEPENDENCY_FIELDS",
|
|
25
|
+
"WheelMetadata",
|
|
26
|
+
"intern_version",
|
|
27
|
+
"load_static_project",
|
|
28
|
+
"parse_metadata",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ``[project].dynamic`` keys that disqualify the static reader.
|
|
33
|
+
# When either appears the build backend may override the declared
|
|
34
|
+
# values, so PEP 621 does not guarantee the table is authoritative.
|
|
35
|
+
_DYNAMIC_FIELD_BLOCKERS = frozenset({"dependencies", "optional-dependencies"})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_static_project(text: str) -> dict[str, Any] | None:
|
|
39
|
+
"""Return the ``[project]`` table when it can be trusted as static.
|
|
40
|
+
|
|
41
|
+
Returns ``None`` when the TOML cannot be parsed, the
|
|
42
|
+
``[project]`` table is missing or malformed, or
|
|
43
|
+
``project.dynamic`` includes ``dependencies`` /
|
|
44
|
+
``optional-dependencies`` (in which case the static reader can
|
|
45
|
+
not provide either).
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
data = tomli.loads(text)
|
|
49
|
+
except tomli.TOMLDecodeError:
|
|
50
|
+
return None
|
|
51
|
+
project = data.get("project")
|
|
52
|
+
if not isinstance(project, dict):
|
|
53
|
+
return None
|
|
54
|
+
dynamic_raw = project.get("dynamic")
|
|
55
|
+
if isinstance(dynamic_raw, list):
|
|
56
|
+
dynamic_set = {d for d in dynamic_raw if isinstance(d, str)}
|
|
57
|
+
if _DYNAMIC_FIELD_BLOCKERS & dynamic_set:
|
|
58
|
+
return None
|
|
59
|
+
return project
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# PEP 643 dependency-affecting METADATA fields, lowercased.
|
|
63
|
+
# Intersect with WheelMetadata.dynamic to detect wheels whose dep
|
|
64
|
+
# declarations may change at build time.
|
|
65
|
+
DEPENDENCY_FIELDS = frozenset({"requires-dist", "provides-extra"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@lru_cache(maxsize=16384)
|
|
69
|
+
def _parse_requirement_cached(req_str: str) -> Requirement:
|
|
70
|
+
"""Cache ``Requirement(req_str)`` parsing across wheel metadata.
|
|
71
|
+
|
|
72
|
+
The same dep strings (``numpy>=1.26``, ``pydantic<3``, etc.) recur
|
|
73
|
+
across many wheels in a dependency graph. ``Requirement`` exposes
|
|
74
|
+
only read operations (specifier, marker, extras, name) so sharing
|
|
75
|
+
parsed objects is safe.
|
|
76
|
+
"""
|
|
77
|
+
return Requirement(req_str)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@lru_cache(maxsize=65536)
|
|
81
|
+
def intern_version(version_str: str) -> Version:
|
|
82
|
+
"""Return a shared :class:`Version` for ``version_str``.
|
|
83
|
+
|
|
84
|
+
The same version string recurs across the per-platform wheels of a
|
|
85
|
+
project (and across projects that publish the same number). Sharing
|
|
86
|
+
the parsed object saves the PEP 440 regex walk on every duplicate.
|
|
87
|
+
``Version`` is immutable in ``packaging``, so the shared instance
|
|
88
|
+
is safe.
|
|
89
|
+
"""
|
|
90
|
+
return Version(version_str)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class WheelMetadata:
|
|
95
|
+
"""Parsed fields from a wheel's METADATA file."""
|
|
96
|
+
|
|
97
|
+
name: str
|
|
98
|
+
version: Version
|
|
99
|
+
requires_python: SpecifierSet | None = None
|
|
100
|
+
requires_dist: list[Requirement] = field(default_factory=list)
|
|
101
|
+
provides_extra: list[str] = field(default_factory=list)
|
|
102
|
+
metadata_version: str | None = None
|
|
103
|
+
dynamic: frozenset[str] = field(default_factory=frozenset)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_metadata(data: str | bytes) -> WheelMetadata:
|
|
107
|
+
"""Parse a METADATA file and return the fields needed for resolution."""
|
|
108
|
+
if isinstance(data, bytes):
|
|
109
|
+
data = data.decode("utf-8")
|
|
110
|
+
|
|
111
|
+
msg = email.parser.Parser().parsestr(data)
|
|
112
|
+
|
|
113
|
+
name = msg.get("Name")
|
|
114
|
+
if name is None:
|
|
115
|
+
err = "METADATA missing required Name field"
|
|
116
|
+
raise ValueError(err)
|
|
117
|
+
|
|
118
|
+
version_str = msg.get("Version")
|
|
119
|
+
if version_str is None:
|
|
120
|
+
err = "METADATA missing required Version field"
|
|
121
|
+
raise ValueError(err)
|
|
122
|
+
|
|
123
|
+
requires_python_str = msg.get("Requires-Python")
|
|
124
|
+
requires_python = SpecifierSet(requires_python_str) if requires_python_str else None
|
|
125
|
+
|
|
126
|
+
requires_dist = [
|
|
127
|
+
_parse_requirement_cached(r) for r in msg.get_all("Requires-Dist") or []
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
provides_extra = list(msg.get_all("Provides-Extra") or [])
|
|
131
|
+
|
|
132
|
+
metadata_version = msg.get("Metadata-Version")
|
|
133
|
+
# PEP 643 field names are case-insensitive per RFC 822; normalise so
|
|
134
|
+
# downstream lookups don't depend on the producer's capitalisation.
|
|
135
|
+
dynamic = frozenset(d.lower() for d in msg.get_all("Dynamic") or [])
|
|
136
|
+
|
|
137
|
+
return WheelMetadata(
|
|
138
|
+
name=name,
|
|
139
|
+
version=intern_version(version_str),
|
|
140
|
+
requires_python=requires_python,
|
|
141
|
+
requires_dist=requires_dist,
|
|
142
|
+
provides_extra=provides_extra,
|
|
143
|
+
metadata_version=metadata_version,
|
|
144
|
+
dynamic=dynamic,
|
|
145
|
+
)
|