77The v2 format stores a JSON pointer file as a TUF target at:
88
99 targets/<project>/<version>.json (versioned pointer)
10+ targets/<project>/latest.json (copy of latest stable version pointer)
1011
1112Target files are content-addressed on S3 (consistent-snapshot format):
12- the actual file at ``targets/<project>/<version >.json`` is stored as
13- ``targets/<project>/{sha256}.{version }.json``. The TUF Updater resolves
14- the hash-prefixed path automatically via ``N.targets.json`` metadata.
13+ the actual file at ``targets/<project>/<name >.json`` is stored as
14+ ``targets/<project>/{sha256}.{name }.json``. The TUF Updater resolves
15+ the hash-prefixed path automatically via TUF metadata.
1516
1617Each pointer file contains:
1718
3435import json
3536import logging
3637import tempfile
37- import urllib .error
3838import urllib .request
3939from pathlib import Path
4040
41- from packaging .version import InvalidVersion , Version
42- from tuf .api .metadata import Metadata , Snapshot , Targets , Timestamp
4341from tuf .ngclient import Updater
4442from tuf .ngclient .config import UpdaterConfig
4543
@@ -74,10 +72,7 @@ def __init__(
7472 self ._disable_verification = disable_verification
7573
7674 if disable_verification :
77- logger .warning (
78- 'Running with TUF verification disabled. '
79- 'Integrity is protected only by TLS (HTTPS).'
80- )
75+ logger .warning ('Running with TUF verification disabled. Integrity is protected only by TLS (HTTPS).' )
8176
8277 # ------------------------------------------------------------------
8378 # Internal helpers
@@ -99,51 +94,17 @@ def _make_updater(self, metadata_dir: Path, target_dir: Path) -> Updater:
9994 )
10095
10196 @staticmethod
102- def _resolve_version (project : str , version : str | None , targets : Targets ) -> str :
103- """Return the concrete version to fetch.
97+ def _target_path (project : str , version : str | None ) -> str :
98+ name = version if version is not None else 'latest'
99+ return f'{ project } /{ name } .json'
104100
105- If *version* is given, return it as-is (TUF will raise if absent).
106- If None, scan *targets* for all ``<project>/<ver>.json`` entries and
107- return the highest stable PEP 440 version.
108- """
109- if version is not None :
110- return version
111-
112- prefix = f"{ project } /"
113- stable : list [Version ] = []
114- for target_path in targets .targets :
115- if not (target_path .startswith (prefix ) and target_path .endswith (".json" )):
116- continue
117- stem = target_path [len (prefix ): - len (".json" )]
118- try :
119- v = Version (stem )
120- except InvalidVersion :
121- continue
122- if not v .is_prerelease :
123- stable .append (v )
124-
125- if not stable :
126- raise TargetNotFoundError (f"No stable releases found for { project !r} in targets metadata" )
127- return str (max (stable ))
128-
129- def _fetch_targets_unverified (self ) -> Targets :
130- """Fetch targets metadata without verifying TUF signatures.
131-
132- Walks timestamp → snapshot → targets to resolve the current versioned
133- targets file and returns the parsed Targets signed payload.
134- """
135- base = self ._repository_url
136-
137- with urllib .request .urlopen (f'{ base } /metadata/timestamp.json' ) as r :
138- ts : Metadata [Timestamp ] = Metadata [Timestamp ].from_bytes (r .read ())
139- snap_ver = ts .signed .snapshot_meta .version
140-
141- with urllib .request .urlopen (f'{ base } /metadata/{ snap_ver } .snapshot.json' ) as r :
142- snap : Metadata [Snapshot ] = Metadata [Snapshot ].from_bytes (r .read ())
143- tgt_ver = snap .signed .meta ['targets.json' ].version
101+ @staticmethod
102+ def _wheel_filename (project : str , version : str ) -> str :
103+ distribution = project .replace ('-' , '_' )
104+ return f'{ distribution } -{ version } -py3-none-any.whl'
144105
145- with urllib . request . urlopen ( f' { base } /metadata/ { tgt_ver } .targets.json' ) as r :
146- return Metadata [ Targets ]. from_bytes ( r . read ()). signed
106+ def _direct_wheel_url ( self , project : str , version : str ) -> str :
107+ return f' { self . _repository_url } /wheels/ { project } / { self . _wheel_filename ( project , version ) } '
147108
148109 # ------------------------------------------------------------------
149110 # Public API
@@ -152,9 +113,7 @@ def _fetch_targets_unverified(self) -> Targets:
152113 def get_pointer (self , project : str , version : str | None = None ) -> dict :
153114 """Return the pointer JSON for *project* at *version*.
154115
155- Resolves *version* = None to the latest stable release by scanning
156- ``N.targets.json``. The pointer file is fetched via its hash-prefixed
157- consistent-snapshot path.
116+ Resolves *version* = None by fetching ``<project>/latest.json``.
158117 Raises ``TargetNotFoundError`` if the target is absent from the TUF repo.
159118 """
160119 with tempfile .TemporaryDirectory () as tmp :
@@ -163,42 +122,15 @@ def get_pointer(self, project: str, version: str | None = None) -> dict:
163122 metadata_dir .mkdir ()
164123 target_dir .mkdir ()
165124
166- if self ._disable_verification :
167- targets = self ._fetch_targets_unverified ()
168- resolved = self ._resolve_version (project , version , targets )
169- target_path = f'{ project } /{ resolved } .json'
170-
171- entry = targets .targets .get (target_path )
172- if entry is None :
173- raise TargetNotFoundError (f'Target not found: { target_path } ' )
174-
175- sha256 = entry .hashes .get ('sha256' , '' )
176- url = f'{ self ._repository_url } /targets/{ project } /{ sha256 } .{ resolved } .json'
177- try :
178- with urllib .request .urlopen (url ) as resp :
179- return json .loads (resp .read ())
180- except urllib .error .HTTPError as exc :
181- if exc .code == 404 :
182- raise TargetNotFoundError (f'Pointer not found: { url } ' ) from exc
183- raise
184-
125+ target_path = self ._target_path (project , version )
185126 self ._bootstrap_metadata_dir (metadata_dir )
186127 updater = self ._make_updater (metadata_dir , target_dir )
187128 updater .refresh ()
188129
189- # Parse the downloaded N.targets.json to resolve version and
190- # enumerate the hash-prefixed path the Updater will fetch.
191- candidates = sorted (metadata_dir .glob ('*.targets.json' ))
192- if not candidates :
193- raise TargetNotFoundError (f'No targets metadata after refresh for { project !r} ' )
194- targets = Metadata [Targets ].from_file (str (candidates [- 1 ])).signed
195-
196- resolved = self ._resolve_version (project , version , targets )
197- target_path = f'{ project } /{ resolved } .json'
198-
199130 target_info = updater .get_targetinfo (target_path )
200131 if target_info is None :
201- raise TargetNotFoundError (f'No TUF target for { project !r} version { resolved !r} ' )
132+ label = version if version is not None else 'latest stable'
133+ raise TargetNotFoundError (f'No TUF target for { project !r} version { label !r} ' )
202134
203135 pointer_path = target_dir / target_path
204136 pointer_path .parent .mkdir (parents = True , exist_ok = True )
@@ -218,20 +150,28 @@ def download(
218150 Raises ``DigestMismatch`` if the wheel digest or length does not match
219151 the pointer file.
220152 """
221- pointer = self .get_pointer (project , version )
222- # Use the caller-supplied repository URL for the wheel fetch so that
223- # --repository can point at a staging bucket and override the prod URL
224- # baked into the pointer file. This allows pre-promotion validation
225- # against staging S3 without modifying pointer content.
226- wheel_url = self ._repository_url + pointer ['wheel_path' ]
227- wheel_filename = Path (pointer ['wheel_path' ]).name
153+ if self ._disable_verification :
154+ if version is None :
155+ raise TargetNotFoundError ('unsafe-disable-verification requires an explicit --version' )
156+ wheel_url = self ._direct_wheel_url (project , version )
157+ wheel_filename = self ._wheel_filename (project , version )
158+ pointer = None
159+ else :
160+ pointer = self .get_pointer (project , version )
161+ # Use the caller-supplied repository URL for the wheel fetch so that
162+ # --repository can point at a staging bucket and override the prod URL
163+ # baked into the pointer file. This allows pre-promotion validation
164+ # against staging S3 without modifying pointer content.
165+ wheel_url = self ._repository_url + pointer ['wheel_path' ]
166+ wheel_filename = Path (pointer ['wheel_path' ]).name
167+
228168 dest = (dest_dir or Path (tempfile .mkdtemp ())) / wheel_filename
229169
230170 logger .info ('Downloading wheel from %s' , wheel_url )
231171 with urllib .request .urlopen (wheel_url ) as resp :
232172 content = resp .read ()
233173
234- if not self . _disable_verification :
174+ if pointer is not None :
235175 actual_digest = hashlib .sha256 (content ).hexdigest ()
236176 if actual_digest != pointer ['digest' ]:
237177 raise DigestMismatch (project , pointer ['digest' ], actual_digest )
0 commit comments