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/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
+ )