-
Notifications
You must be signed in to change notification settings - Fork 39
feat: add alternative download methods to resolver API #871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| import datetime | ||
| import enum | ||
| import functools | ||
| import logging | ||
| import os | ||
|
|
@@ -14,7 +15,7 @@ | |
| from collections.abc import Iterable | ||
| from operator import attrgetter | ||
| from platform import python_version | ||
| from urllib.parse import quote, unquote, urljoin, urlparse | ||
| from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit | ||
|
|
||
| import pypi_simple | ||
| import resolvelib | ||
|
|
@@ -180,11 +181,42 @@ def resolve_from_provider( | |
| raise ValueError(f"Unable to resolve {req}") | ||
|
|
||
|
|
||
| class RetrieveMethod(enum.StrEnum): | ||
| tarball = "tarball" | ||
| git_https = "git+https" | ||
| git_ssh = "git+ssh" | ||
|
|
||
| @classmethod | ||
| def from_url(cls, download_url) -> tuple[RetrieveMethod, str, str | None]: | ||
| """Parse a download URL into method, url, reference""" | ||
| scheme, netloc, path, query, fragment = urlsplit( | ||
| download_url, allow_fragments=False | ||
| ) | ||
| match scheme: | ||
| case "https": | ||
| return RetrieveMethod.tarball, download_url, None | ||
| case "git+https": | ||
| method = RetrieveMethod.git_https | ||
| case "git+ssh": | ||
| method = RetrieveMethod.git_ssh | ||
| case _: | ||
| raise ValueError(f"unsupported download URL {download_url!r}") | ||
LalatenduMohanty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # remove git+ | ||
| scheme = scheme[4:] | ||
| # split off @ revision | ||
| if "@" not in path: | ||
| raise ValueError(f"git download url {download_url!r} is missing '@ref'") | ||
| path, ref = path.rsplit("@", 1) | ||
| return method, urlunsplit((scheme, netloc, path, query, fragment)), ref | ||
|
|
||
|
|
||
| def get_project_from_pypi( | ||
| project: str, | ||
| extras: typing.Iterable[str], | ||
| sdist_server_url: str, | ||
| ignore_platform: bool = False, | ||
| *, | ||
| override_download_url: str | None = None, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear how a caller would know what an alternative download site might be, and why that would be anything other than another package index server that could have been passed as the sdist_server_url parameter. What's the use case for using on package index to resolve a version and another server to act as the source of the download? Shouldn't the resolver look at the place where it's going to download the package to see what versions are available there? |
||
| ) -> Candidates: | ||
| """Return candidates created from the project name and extras.""" | ||
| found_candidates: set[str] = set() | ||
|
|
@@ -345,14 +377,19 @@ def get_project_from_pypi( | |
| ignored_candidates.add(dp.filename) | ||
| continue | ||
|
|
||
| if override_download_url is None: | ||
| url = dp.url | ||
| else: | ||
| url = override_download_url.format(version=version) | ||
|
|
||
| upload_time = dp.upload_time | ||
| if upload_time is not None: | ||
| upload_time = upload_time.astimezone(datetime.UTC) | ||
|
|
||
| c = Candidate( | ||
| name=name, | ||
| version=version, | ||
| url=dp.url, | ||
| url=url, | ||
| extras=tuple(sorted(extras)), | ||
| is_sdist=is_sdist, | ||
| build_tag=build_tag, | ||
|
|
@@ -603,6 +640,7 @@ def __init__( | |
| ignore_platform: bool = False, | ||
| *, | ||
| use_resolver_cache: bool = True, | ||
| override_download_url: str | None = None, | ||
| ): | ||
| super().__init__( | ||
| constraints=constraints, | ||
|
|
@@ -613,6 +651,7 @@ def __init__( | |
| self.include_wheels = include_wheels | ||
| self.sdist_server_url = sdist_server_url | ||
| self.ignore_platform = ignore_platform | ||
| self.override_download_url = override_download_url | ||
|
|
||
| @property | ||
| def cache_key(self) -> str: | ||
|
|
@@ -625,9 +664,10 @@ def cache_key(self) -> str: | |
| def find_candidates(self, identifier: str) -> Candidates: | ||
| return get_project_from_pypi( | ||
| identifier, | ||
| set(), | ||
| self.sdist_server_url, | ||
| self.ignore_platform, | ||
| extras=set(), | ||
| sdist_server_url=self.sdist_server_url, | ||
| ignore_platform=self.ignore_platform, | ||
| override_download_url=self.override_download_url, | ||
| ) | ||
|
|
||
| def validate_candidate( | ||
|
|
@@ -803,6 +843,7 @@ def __init__( | |
| *, | ||
| req_type: RequirementType | None = None, | ||
| use_resolver_cache: bool = True, | ||
| retrieve_method: RetrieveMethod = RetrieveMethod.tarball, | ||
| ): | ||
| super().__init__( | ||
| constraints=constraints, | ||
|
|
@@ -813,6 +854,7 @@ def __init__( | |
| ) | ||
| self.organization = organization | ||
| self.repo = repo | ||
| self.retrieve_method = retrieve_method | ||
|
|
||
| @property | ||
| def cache_key(self) -> str: | ||
|
|
@@ -847,7 +889,14 @@ def _find_tags( | |
| logger.debug(f"{identifier}: match function ignores {tagname}") | ||
| continue | ||
| assert isinstance(version, Version) | ||
| url = entry["tarball_url"] | ||
|
|
||
| match self.retrieve_method: | ||
| case RetrieveMethod.tarball: | ||
| url = entry["tarball_url"] | ||
| case RetrieveMethod.git_https: | ||
| url = f"git+https://{self.host}/{self.organization}/{self.repo}.git@{tagname}" | ||
| case RetrieveMethod.git_ssh: | ||
| url = f"git+ssh://git@{self.host}/{self.organization}/{self.repo}.git@{tagname}" | ||
|
|
||
| # Github tag API endpoint does not include commit date information. | ||
| # It would be too expensive to query every commit API endpoint. | ||
|
|
@@ -880,6 +929,7 @@ def __init__( | |
| *, | ||
| req_type: RequirementType | None = None, | ||
| use_resolver_cache: bool = True, | ||
| retrieve_method: RetrieveMethod = RetrieveMethod.tarball, | ||
| ) -> None: | ||
| super().__init__( | ||
| constraints=constraints, | ||
|
|
@@ -889,6 +939,9 @@ def __init__( | |
| matcher=matcher, | ||
| ) | ||
| self.server_url = server_url.rstrip("/") | ||
| self.server_hostname = urlparse(server_url).hostname | ||
LalatenduMohanty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if not self.server_hostname: | ||
| raise ValueError(f"invalid {server_url=}") | ||
| self.project_path = project_path.lstrip("/") | ||
| # URL-encode the project path as required by GitLab API. | ||
| # The safe="" parameter tells quote() to encode ALL characters, | ||
|
|
@@ -899,6 +952,7 @@ def __init__( | |
| self.api_url = ( | ||
| f"{self.server_url}/api/v4/projects/{encoded_path}/repository/tags" | ||
| ) | ||
| self.retrieve_method = retrieve_method | ||
|
|
||
| @property | ||
| def cache_key(self) -> str: | ||
|
|
@@ -927,8 +981,14 @@ def _find_tags( | |
| continue | ||
| assert isinstance(version, Version) | ||
|
|
||
| archive_path: str = f"{self.project_path}/-/archive/{tagname}/{self.project_path.split('/')[-1]}-{tagname}.tar.gz" | ||
| url = urljoin(self.server_url, archive_path) | ||
| match self.retrieve_method: | ||
| case RetrieveMethod.tarball: | ||
| archive_path: str = f"{self.project_path}/-/archive/{tagname}/{self.project_path.split('/')[-1]}-{tagname}.tar.gz" | ||
| url = urljoin(self.server_url, archive_path) | ||
| case RetrieveMethod.git_https: | ||
| url = f"git+https://{self.server_hostname}/{self.project_path}.git@{tagname}" | ||
| case RetrieveMethod.git_ssh: | ||
| url = f"git+ssh://git@{self.server_hostname}/{self.project_path}.git@{tagname}" | ||
|
|
||
| # get tag creation time, fall back to commit creation time | ||
| created_at_str: str | None = entry.get("created_at") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.