|
3 | 3 | from functools import partial |
4 | 4 | from urllib.parse import urljoin, urlparse |
5 | 5 |
|
6 | | -from aiohttp import ClientError, ClientResponseError |
| 6 | +from aiohttp import ClientError |
7 | 7 | from bandersnatch.configuration import BandersnatchConfig |
8 | 8 | from bandersnatch.master import Master |
9 | 9 | from bandersnatch.mirror import Mirror |
10 | | -from lxml.etree import LxmlError |
11 | 10 | from packaging.requirements import Requirement |
12 | 11 | from pypi_simple import IndexPage |
13 | 12 |
|
@@ -169,41 +168,36 @@ def __init__(self, serial, master, workers, deferred_download, python_stage, pro |
169 | 168 |
|
170 | 169 | async def determine_packages_to_sync(self): |
171 | 170 | """ |
172 | | - Calling this means that includes wasn't specified, |
173 | | - so try to get all of the packages from Mirror (hopefully PyPi) |
| 171 | + Called when includes wasn't specified. List all projects from the remote |
| 172 | + via the PEP 691 Simple JSON API, falling back to HTML /simple/. |
174 | 173 | """ |
175 | | - number_xmlrpc_attempts = 3 |
176 | | - for attempt in range(number_xmlrpc_attempts): |
177 | | - logger.info("Attempt {} to get package list from {}".format(attempt, self.master.url)) |
178 | | - try: |
179 | | - if not self.synced_serial: |
180 | | - logger.info("Syncing all packages.") |
181 | | - # First get the current serial, then start to sync. |
182 | | - all_packages = await self.master.all_packages() |
183 | | - self.packages_to_sync.update(all_packages) |
184 | | - self.target_serial = max( |
185 | | - [self.synced_serial] + [int(v) for v in self.packages_to_sync.values()] |
186 | | - ) |
187 | | - else: |
188 | | - logger.info("Syncing based on changelog.") |
189 | | - changed_packages = await self.master.changed_packages(self.synced_serial) |
190 | | - self.packages_to_sync.update(changed_packages) |
191 | | - self.target_serial = max( |
192 | | - [self.synced_serial] + [int(v) for v in self.packages_to_sync.values()] |
193 | | - ) |
194 | | - break |
195 | | - except (ClientError, ClientResponseError, LxmlError): |
196 | | - # Retry if XMLRPC endpoint failed, server might not support it. |
197 | | - continue |
198 | | - else: |
199 | | - logger.info("Failed to get package list using XMLRPC, trying parse simple page.") |
| 174 | + logger.info("Syncing all packages from %s", self.master.url) |
| 175 | + try: |
| 176 | + simple_index = await self.master.fetch_simple_index() |
| 177 | + if not isinstance(simple_index, dict) or "projects" not in simple_index: |
| 178 | + raise ValueError("Simple JSON index is missing a projects list") |
| 179 | + for project in simple_index["projects"]: |
| 180 | + name = project.get("name") |
| 181 | + if name is None: |
| 182 | + continue |
| 183 | + # _last-serial is a PyPI extension; default to 0 when absent |
| 184 | + self.packages_to_sync[name] = project.get("_last-serial", 0) |
| 185 | + self.target_serial = max( |
| 186 | + [self.synced_serial or 0] + [int(v) for v in self.packages_to_sync.values()] |
| 187 | + ) |
| 188 | + except (ClientError, ValueError, TypeError, AttributeError) as exc: |
| 189 | + logger.info( |
| 190 | + "Failed to list packages via Simple JSON API (%s); " |
| 191 | + "falling back to HTML simple index.", |
| 192 | + exc, |
| 193 | + ) |
200 | 194 | url = urljoin(self.remote.url, "simple/") |
201 | 195 | downloader = self.remote.get_downloader(url=url) |
202 | 196 | result = await downloader.run() |
203 | 197 | with open(result.path) as f: |
204 | 198 | index = IndexPage.from_html(f.read()) |
205 | | - self.packages_to_sync.update({p: 0 for p in index.projects}) |
206 | | - self.target_serial = result.headers.get(PYPI_LAST_SERIAL, 0) |
| 199 | + self.packages_to_sync.update({p: 0 for p in index.projects}) |
| 200 | + self.target_serial = result.headers.get(PYPI_LAST_SERIAL, 0) |
207 | 201 |
|
208 | 202 | self._filter_packages() |
209 | 203 | if self.target_serial: |
|
0 commit comments