diff --git a/CHANGES.md b/CHANGES.md index 060433304..fa488f024 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,15 @@ # Release Notes +## 2.19.0 + +This release adds support for a new `--pre-resolved-dists` resolver as +an alternative to the existing Pip resolver, `--lock` resolver and +`--pex-repository` resolvers. Using `--pre-resolved-dists dists/dir/` +behaves much like `--no-pypi --find-links dists/dir/` except that it is +roughly 3x faster. + +* Support `--pre-resolved-dists` resolver. (#2512) + ## 2.18.1 This release fixes `--scie-name-style platform-parent-dir` introduced in diff --git a/pex/bin/pex.py b/pex/bin/pex.py index ebb462939..a22fdd70c 100755 --- a/pex/bin/pex.py +++ b/pex/bin/pex.py @@ -53,6 +53,7 @@ from pex.resolve.resolver_configuration import ( LockRepositoryConfiguration, PexRepositoryConfiguration, + PreResolvedConfiguration, ) from pex.resolve.resolver_options import create_pip_configuration from pex.resolve.resolvers import Unsatisfiable, sorted_requirements @@ -136,7 +137,9 @@ def configure_clp_pex_resolution(parser): ), ) - resolver_options.register(group, include_pex_repository=True, include_lock=True) + resolver_options.register( + group, include_pex_repository=True, include_lock=True, include_pre_resolved=True + ) group.add_argument( "--pex-path", @@ -1011,25 +1014,26 @@ def build_pex( DependencyConfiguration.from_pex_info(requirements_pex_info) ) + if isinstance(resolver_configuration, (LockRepositoryConfiguration, PreResolvedConfiguration)): + pip_configuration = resolver_configuration.pip_configuration + elif isinstance(resolver_configuration, PexRepositoryConfiguration): + # TODO(John Sirois): Consider finding a way to support custom --index and --find-links in + # this case. I.E.: I use a corporate index to build a PEX repository and now I want to + # build a --project PEX whose pyproject.toml build-system.requires should be resolved from + # that corporate index. + pip_configuration = try_( + finalize_resolve_config( + create_pip_configuration(options), targets=targets, context="--project building" + ) + ) + else: + pip_configuration = resolver_configuration + project_dependencies = OrderedSet() # type: OrderedSet[Requirement] with TRACER.timed( "Adding distributions built from local projects and collecting their requirements: " "{projects}".format(projects=" ".join(options.projects)) ): - if isinstance(resolver_configuration, LockRepositoryConfiguration): - pip_configuration = resolver_configuration.pip_configuration - elif isinstance(resolver_configuration, PexRepositoryConfiguration): - # TODO(John Sirois): Consider finding a way to support custom --index and --find-links in this case. - # I.E.: I use a corporate index to build a PEX repository and now I want to build a --project PEX - # whose pyproject.toml build-system.requires should be resolved from that corporate index. - pip_configuration = try_( - finalize_resolve_config( - create_pip_configuration(options), targets=targets, context="--project building" - ) - ) - else: - pip_configuration = resolver_configuration - projects = project.get_projects(options) built_projects = projects.build( targets=targets, diff --git a/pex/cli/commands/lock.py b/pex/cli/commands/lock.py index 2e48fa1c2..377454e4a 100644 --- a/pex/cli/commands/lock.py +++ b/pex/cli/commands/lock.py @@ -427,6 +427,7 @@ def _add_resolve_options(cls, parser): cls._create_resolver_options_group(parser), include_pex_repository=False, include_lock=False, + include_pre_resolved=False, ) @classmethod diff --git a/pex/cli/commands/venv.py b/pex/cli/commands/venv.py index 3e8625941..339136f92 100644 --- a/pex/cli/commands/venv.py +++ b/pex/cli/commands/venv.py @@ -110,7 +110,9 @@ def _add_create_arguments(cls, parser): ) installer_options.register(parser) target_options.register(parser, include_platforms=True) - resolver_options.register(parser, include_pex_repository=True, include_lock=True) + resolver_options.register( + parser, include_pex_repository=True, include_lock=True, include_pre_resolved=True + ) requirement_options.register(parser) @classmethod diff --git a/pex/common.py b/pex/common.py index 693c5f231..0799e4969 100644 --- a/pex/common.py +++ b/pex/common.py @@ -450,10 +450,11 @@ def can_write_dir(path): def touch(file): - # type: (Text) -> None + # type: (_Text) -> _Text """Equivalent of unix `touch path`.""" with safe_open(file, "a"): os.utime(file, None) + return file class Chroot(object): diff --git a/pex/dist_metadata.py b/pex/dist_metadata.py index 690058724..9140db0de 100644 --- a/pex/dist_metadata.py +++ b/pex/dist_metadata.py @@ -75,14 +75,65 @@ class InvalidMetadataError(MetadataError): """Indicates a metadata value that is invalid.""" +def is_tar_sdist(path): + # type: (Text) -> bool + # N.B.: PEP-625 (https://peps.python.org/pep-0625/) says sdists must use .tar.gz, but we + # have a known example of tar.bz2 in the wild in python-constraint 1.4.0 on PyPI: + # https://pypi.org/project/python-constraint/1.4.0/#files + # This probably all stems from the legacy `python setup.py sdist` as last described here: + # https://docs.python.org/3.11/distutils/sourcedist.html + # There was a move to reject exotic formats in PEP-527 in 2016 and the historical sdist + # formats appear to be listed here: https://peps.python.org/pep-0527/#file-extensions + # A query on the PyPI dataset shows: + # + # SELECT + # REGEXP_EXTRACT(path, r'\.([^.]+|tar\.[^.]+|tar)$') as extension, + # count(*) as count + # FROM `bigquery-public-data.pypi.distribution_metadata` + # group by extension + # order by count desc + # + # | extension | count | + # |-----------|---------| + # | whl | 6332494 | + # * | tar.gz | 5283102 | + # | egg | 135940 | + # * | zip | 108532 | + # | exe | 18452 | + # * | tar.bz2 | 3857 | + # | msi | 625 | + # | rpm | 603 | + # * | tgz | 226 | + # | dmg | 47 | + # | deb | 36 | + # * | tar.zip | 2 | + # * | ZIP | 1 | + return path.lower().endswith((".tar.gz", ".tgz", ".tar.bz2")) + + +def is_zip_sdist(path): + # type: (Text) -> bool + return path.lower().endswith(".zip") + + +def is_sdist(path): + # type: (Text) -> bool + return is_tar_sdist(path) or is_zip_sdist(path) + + +def is_wheel(path): + # type: (Text) -> bool + return path.lower().endswith(".whl") + + def _strip_sdist_path(sdist_path): # type: (Text) -> Optional[Text] - if not sdist_path.endswith((".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz", ".zip")): + if not is_sdist(sdist_path): return None sdist_basename = os.path.basename(sdist_path) filename, _ = os.path.splitext(sdist_basename) - if filename.endswith(".tar"): + if filename.lower().endswith(".tar"): filename, _ = os.path.splitext(filename) return filename @@ -194,8 +245,19 @@ def read_function(rel_path): ) +def _read_from_zip( + zip_location, # type: str + rel_path, # type: Text +): + # type: (...) -> bytes + with open_zip(zip_location) as zf: + return zf.read(rel_path) + + def find_wheel_metadata(location): # type: (Text) -> Optional[MetadataFiles] + + read_function = functools.partial(_read_from_zip, location) with open_zip(location) as zf: for name in zf.namelist(): if name.endswith("/"): @@ -218,11 +280,6 @@ def find_wheel_metadata(location): if dist_info_dir == head and tail != metadata_file_name: files.append(rel_path) - def read_function(rel_path): - # type: (Text) -> bytes - with open_zip(location) as zf: - return zf.read(rel_path) - return MetadataFiles( metadata=DistMetadataFile( type=MetadataType.DIST_INFO, @@ -330,7 +387,7 @@ def iter_metadata_files( location, MetadataType.DIST_INFO, "*.dist-info", "METADATA" ) ) - elif location.endswith(".whl") and zipfile.is_zipfile(location): + elif is_wheel(location) and zipfile.is_zipfile(location): metadata_files = find_wheel_metadata(location) if metadata_files: listing.append(metadata_files) @@ -341,13 +398,11 @@ def iter_metadata_files( ) ) elif MetadataType.PKG_INFO is metadata_type: - if location.endswith(".zip") and zipfile.is_zipfile(location): + if is_zip_sdist(location) and zipfile.is_zipfile(location): metadata_file = find_zip_sdist_metadata(location) if metadata_file: listing.append(MetadataFiles(metadata=metadata_file)) - elif location.endswith( - (".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz") - ) and tarfile.is_tarfile(location): + elif is_tar_sdist(location) and tarfile.is_tarfile(location): metadata_file = find_tar_sdist_metadata(location) if metadata_file: listing.append(MetadataFiles(metadata=metadata_file)) @@ -408,7 +463,7 @@ def from_filename(cls, path): # # The wheel filename convention is specified here: # https://www.python.org/dev/peps/pep-0427/#file-name-convention. - if path.endswith(".whl"): + if is_wheel(path): project_name, version, _ = os.path.basename(path).split("-", 2) return cls(project_name=project_name, version=version) @@ -903,7 +958,7 @@ def of(cls, location): # type: (Text) -> DistributionType.Value if os.path.isdir(location): return cls.INSTALLED - if location.endswith(".whl") and zipfile.is_zipfile(location): + if is_wheel(location) and zipfile.is_zipfile(location): return cls.WHEEL return cls.SDIST diff --git a/pex/environment.py b/pex/environment.py index 2452229fb..1b9317ab4 100644 --- a/pex/environment.py +++ b/pex/environment.py @@ -12,17 +12,18 @@ from pex import dist_metadata, pex_warnings, targets from pex.common import pluralize from pex.dependency_configuration import DependencyConfiguration -from pex.dist_metadata import Distribution, Requirement +from pex.dist_metadata import Distribution, Requirement, is_wheel from pex.fingerprinted_distribution import FingerprintedDistribution from pex.inherit_path import InheritPath from pex.interpreter import PythonInterpreter from pex.layout import ensure_installed, identify_layout from pex.orderedset import OrderedSet -from pex.pep_425 import CompatibilityTags, TagRank +from pex.pep_425 import TagRank from pex.pep_503 import ProjectName from pex.pex_info import PexInfo from pex.targets import Target from pex.third_party.packaging import specifiers +from pex.third_party.packaging.tags import Tag from pex.tracer import TRACER from pex.typing import TYPE_CHECKING @@ -139,7 +140,7 @@ def render_message(self, _target): @attr.s(frozen=True) class _TagMismatch(_UnrankedDistribution): - wheel_tags = attr.ib() # type: CompatibilityTags + wheel_tags = attr.ib() # type: Iterable[Tag] def render_message(self, target): # type: (Target) -> str @@ -332,8 +333,8 @@ def _update_candidate_distributions(self, distribution_iter): def _can_add(self, fingerprinted_dist): # type: (FingerprintedDistribution) -> Union[_RankedDistribution, _UnrankedDistribution] - filename, ext = os.path.splitext(os.path.basename(fingerprinted_dist.location)) - if ext.lower() != ".whl": + filename = os.path.basename(fingerprinted_dist.location) + if not is_wheel(filename): # This supports resolving pex's own vendored distributions which are vendored in a # directory with the project name (`pip/` for pip) and not the corresponding wheel name # (`pip-19.3.1-py2.py3-none-any.whl/` for pip). Pex only vendors universal wheels for @@ -341,23 +342,17 @@ def _can_add(self, fingerprinted_dist): return _RankedDistribution.highest_rank(fingerprinted_dist) try: - wheel_tags = CompatibilityTags.from_wheel(fingerprinted_dist.location) + wheel_eval = self._target.wheel_applies(fingerprinted_dist.distribution) except ValueError: return _InvalidWheelName(fingerprinted_dist, filename) - # There will be multiple parsed tags for compressed tag sets. Ensure we grab the parsed tag - # with highest rank from that expanded set. - best_match = self._target.supported_tags.best_match(wheel_tags) - if best_match is None: - return _TagMismatch(fingerprinted_dist, wheel_tags) + if not wheel_eval.best_match: + return _TagMismatch(fingerprinted_dist, wheel_eval.tags) + if not wheel_eval.applies: + assert wheel_eval.requires_python + return _PythonRequiresMismatch(fingerprinted_dist, wheel_eval.requires_python) - python_requires = dist_metadata.requires_python(fingerprinted_dist.distribution) - if python_requires and not self._target.requires_python_applies( - python_requires, source=fingerprinted_dist.distribution.as_requirement() - ): - return _PythonRequiresMismatch(fingerprinted_dist, python_requires) - - return _RankedDistribution(best_match.rank, fingerprinted_dist) + return _RankedDistribution(wheel_eval.best_match.rank, fingerprinted_dist) def activate(self): # type: () -> Iterable[Distribution] diff --git a/pex/jobs.py b/pex/jobs.py index bae04ccc6..bc75fe03b 100644 --- a/pex/jobs.py +++ b/pex/jobs.py @@ -744,10 +744,10 @@ def iter_map_parallel( # input_items.sort(key=costing_function, reverse=True) - # We want each of the job slots above to process MULTIPROCESSING_MIN_AVERAGE_LOAD on average in - # order to overcome multiprocessing overheads. Of course, if there are fewer available cores - # than that or the user has pinned max jobs lower, we clamp to that. Finally, we always want at - # least two slots to ensure we process input items in parallel. + # We want each of the job slots above to process MULTIPROCESSING_DEFAULT_MIN_AVERAGE_LOAD on + # average in order to overcome multiprocessing overheads. Of course, if there are fewer + # available cores than that or the user has pinned max jobs lower, we clamp to that. Finally, we + # always want at least two slots to ensure we process input items in parallel. pool_size = max(2, min(len(input_items) // min_average_load, _sanitize_max_jobs(max_jobs))) apply_function = functools.partial(_apply_function, function) diff --git a/pex/pep_425.py b/pex/pep_425.py index 2d5f35f4b..a32bf9ec3 100644 --- a/pex/pep_425.py +++ b/pex/pep_425.py @@ -6,6 +6,7 @@ import itertools import os.path +from pex.dist_metadata import is_wheel from pex.orderedset import OrderedSet from pex.rank import Rank from pex.third_party.packaging.tags import Tag, parse_tag @@ -56,14 +57,14 @@ class CompatibilityTags(object): @classmethod def from_wheel(cls, wheel): # type: (str) -> CompatibilityTags - wheel_stem, ext = os.path.splitext(os.path.basename(wheel)) - if ".whl" != ext: + if not is_wheel(wheel): raise ValueError( "Can only calculate wheel tags from a filename that ends in .whl per " "https://peps.python.org/pep-0427/#file-name-convention, given: {wheel!r}".format( wheel=wheel ) ) + wheel_stem, _ = os.path.splitext(os.path.basename(wheel)) # Wheel filename format: https://www.python.org/dev/peps/pep-0427/#file-name-convention # `{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl` wheel_components = wheel_stem.rsplit("-", 3) diff --git a/pex/requirements.py b/pex/requirements.py index 925f076be..8622557ab 100644 --- a/pex/requirements.py +++ b/pex/requirements.py @@ -285,7 +285,7 @@ class VCSScheme(object): def parse_scheme(scheme): - # type: (str) -> Optional[Union[str, ArchiveScheme.Value, VCSScheme]] + # type: (str) -> Union[str, ArchiveScheme.Value, VCSScheme] match = re.match( r""" ^ diff --git a/pex/resolve/configured_resolve.py b/pex/resolve/configured_resolve.py index 76790b7f7..7a7218835 100644 --- a/pex/resolve/configured_resolve.py +++ b/pex/resolve/configured_resolve.py @@ -3,15 +3,18 @@ from __future__ import absolute_import +from pex.common import pluralize from pex.dependency_configuration import DependencyConfiguration from pex.pep_427 import InstallableType from pex.resolve.configured_resolver import ConfiguredResolver from pex.resolve.lock_resolver import resolve_from_lock from pex.resolve.pex_repository_resolver import resolve_from_pex +from pex.resolve.pre_resolved_resolver import resolve_from_dists from pex.resolve.requirement_configuration import RequirementConfiguration from pex.resolve.resolver_configuration import ( LockRepositoryConfiguration, PexRepositoryConfiguration, + PreResolvedConfiguration, ) from pex.resolve.resolvers import ResolveResult from pex.resolver import resolve as resolve_via_pip @@ -34,6 +37,7 @@ def resolve( dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration ): # type: (...) -> ResolveResult + if isinstance(resolver_configuration, LockRepositoryConfiguration): lock = try_(resolver_configuration.parse_lock()) with TRACER.timed( @@ -82,6 +86,27 @@ def resolve( result_type=result_type, dependency_configuration=dependency_configuration, ) + elif isinstance(resolver_configuration, PreResolvedConfiguration): + with TRACER.timed( + "Resolving requirements from {sdist_count} pre-resolved {sdists} and " + "{wheel_count} pre-resolved {wheels}.".format( + sdist_count=len(resolver_configuration.sdists), + sdists=pluralize(resolver_configuration.sdists, "sdist"), + wheel_count=len(resolver_configuration.wheels), + wheels=pluralize(resolver_configuration.wheels, "wheel"), + ) + ): + return resolve_from_dists( + targets=targets, + sdists=resolver_configuration.sdists, + wheels=resolver_configuration.wheels, + requirement_configuration=requirement_configuration, + pip_configuration=resolver_configuration.pip_configuration, + compile=compile_pyc, + ignore_errors=ignore_errors, + result_type=result_type, + dependency_configuration=dependency_configuration, + ) else: with TRACER.timed("Resolving requirements."): return resolve_via_pip( diff --git a/pex/resolve/lock_resolver.py b/pex/resolve/lock_resolver.py index 91e2aa28c..475cc06d2 100644 --- a/pex/resolve/lock_resolver.py +++ b/pex/resolve/lock_resolver.py @@ -15,7 +15,7 @@ from pex.common import pluralize from pex.compatibility import cpu_count from pex.dependency_configuration import DependencyConfiguration -from pex.dist_metadata import Requirement +from pex.dist_metadata import Requirement, is_wheel from pex.network_configuration import NetworkConfiguration from pex.orderedset import OrderedSet from pex.pep_427 import InstallableType @@ -414,7 +414,7 @@ def resolve_from_lock( for resolved_subset in subset_result.subsets: for downloadable_artifact in resolved_subset.resolved.downloadable_artifacts: downloaded_artifact = downloaded_artifacts[downloadable_artifact] - if downloaded_artifact.path.endswith(".whl"): + if is_wheel(downloaded_artifact.path): install_requests.append( InstallRequest( target=resolved_subset.target, diff --git a/pex/resolve/locked_resolve.py b/pex/resolve/locked_resolve.py index eb50fd224..347414d23 100644 --- a/pex/resolve/locked_resolve.py +++ b/pex/resolve/locked_resolve.py @@ -10,7 +10,7 @@ from pex.common import pluralize from pex.dependency_configuration import DependencyConfiguration -from pex.dist_metadata import DistMetadata, Requirement +from pex.dist_metadata import DistMetadata, Requirement, is_sdist, is_wheel from pex.enum import Enum from pex.orderedset import OrderedSet from pex.pep_425 import CompatibilityTags, TagRank @@ -161,61 +161,16 @@ def __lt__(self, other): @attr.s(frozen=True, order=False) class FileArtifact(Artifact): - @staticmethod - def is_zip_sdist(path): - # type: (str) -> bool - - # N.B.: Windows sdists traditionally were released in zip format. - return path.endswith(".zip") - - @staticmethod - def is_tar_sdist(path): - # type: (str) -> bool - - # N.B.: PEP-625 (https://peps.python.org/pep-0625/) says sdists must use .tar.gz, but we - # have a known example of tar.bz2 in the wild in python-constraint 1.4.0 on PyPI: - # https://pypi.org/project/python-constraint/1.4.0/#files - # This probably all stems from the legacy `python setup.py sdist` as last described here: - # https://docs.python.org/3.11/distutils/sourcedist.html - # There was a move to reject exotic formats in PEP-527 in 2016 and the historical sdist - # formats appear to be listed here: https://peps.python.org/pep-0527/#file-extensions - # A query on the PyPI dataset shows: - # - # SELECT - # REGEXP_EXTRACT(path, r'\.([^.]+|tar\.[^.]+|tar)$') as extension, - # count(*) as count - # FROM `bigquery-public-data.pypi.distribution_metadata` - # group by extension - # order by count desc - # - # | extension | count | - # |-----------|---------| - # | whl | 6332494 | - # * | tar.gz | 5283102 | - # | egg | 135940 | - # * | zip | 108532 | - # | exe | 18452 | - # * | tar.bz2 | 3857 | - # | msi | 625 | - # | rpm | 603 | - # * | tgz | 226 | - # | dmg | 47 | - # | deb | 36 | - # * | tar.zip | 2 | - # * | ZIP | 1 | - # - return path.endswith((".tar.gz", ".tgz", ".tar.bz2")) - filename = attr.ib() # type: str @property def is_source(self): # type: () -> bool - return self.is_tar_sdist(self.filename) or self.is_zip_sdist(self.filename) + return is_sdist(self.filename) def parse_tags(self): # type: () -> Iterator[tags.Tag] - if self.filename.endswith(".whl"): + if is_wheel(self.filename): for tag in CompatibilityTags.from_wheel(self.filename): yield tag diff --git a/pex/resolve/lockfile/create.py b/pex/resolve/lockfile/create.py index 1ba05f381..1f1ba0dc2 100644 --- a/pex/resolve/lockfile/create.py +++ b/pex/resolve/lockfile/create.py @@ -14,7 +14,7 @@ from pex.build_system import pep_517 from pex.common import open_zip, pluralize, safe_mkdtemp from pex.dependency_configuration import DependencyConfiguration -from pex.dist_metadata import DistMetadata, ProjectNameAndVersion +from pex.dist_metadata import DistMetadata, ProjectNameAndVersion, is_tar_sdist, is_zip_sdist from pex.fetcher import URLFetcher from pex.jobs import Job, Retain, SpawnedJob, execute_parallel from pex.orderedset import OrderedSet @@ -155,10 +155,10 @@ def _prepare_project_directory(build_request): return target, project extract_dir = os.path.join(safe_mkdtemp(), "project") - if FileArtifact.is_zip_sdist(project): + if is_zip_sdist(project): with open_zip(project) as zf: zf.extractall(extract_dir) - elif FileArtifact.is_tar_sdist(project): + elif is_tar_sdist(project): with tarfile.open(project) as tf: tf.extractall(extract_dir) else: diff --git a/pex/resolve/pre_resolved_resolver.py b/pex/resolve/pre_resolved_resolver.py new file mode 100644 index 000000000..cf91d923f --- /dev/null +++ b/pex/resolve/pre_resolved_resolver.py @@ -0,0 +1,216 @@ +# Copyright 2024 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import + +import hashlib +import os +from collections import defaultdict + +from pex.dependency_configuration import DependencyConfiguration +from pex.dist_metadata import Distribution, Requirement +from pex.fingerprinted_distribution import FingerprintedDistribution +from pex.interpreter import PythonInterpreter +from pex.jobs import iter_map_parallel +from pex.orderedset import OrderedSet +from pex.pep_427 import InstallableType +from pex.pep_503 import ProjectName +from pex.pip.tool import PackageIndexConfiguration +from pex.requirements import LocalProjectRequirement +from pex.resolve.configured_resolver import ConfiguredResolver +from pex.resolve.locked_resolve import Artifact, FileArtifact, LockedRequirement, LockedResolve +from pex.resolve.requirement_configuration import RequirementConfiguration +from pex.resolve.resolved_requirement import ArtifactURL, Fingerprint, Pin +from pex.resolve.resolver_configuration import PipConfiguration +from pex.resolve.resolvers import ( + ResolvedDistribution, + ResolveResult, + check_resolve, + sorted_requirements, +) +from pex.resolver import BuildAndInstallRequest, BuildRequest, InstallRequest +from pex.result import try_ +from pex.sorted_tuple import SortedTuple +from pex.targets import Targets +from pex.tracer import TRACER +from pex.typing import TYPE_CHECKING +from pex.util import CacheHelper + +if TYPE_CHECKING: + from typing import DefaultDict, Dict, Iterable, List, Tuple + + +def _fingerprint_dist(dist_path): + # type: (str) -> Tuple[str, str] + return dist_path, CacheHelper.hash(dist_path, hasher=hashlib.sha256) + + +def resolve_from_dists( + targets, # type: Targets + sdists, # type: Iterable[str] + wheels, # type: Iterable[str] + requirement_configuration, # type: RequirementConfiguration + pip_configuration=PipConfiguration(), # type: PipConfiguration + compile=False, # type: bool + ignore_errors=False, # type: bool + result_type=InstallableType.INSTALLED_WHEEL_CHROOT, # type: InstallableType.Value + dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration +): + # type: (...) -> ResolveResult + + unique_targets = targets.unique_targets() + + direct_requirements = requirement_configuration.parse_requirements( + pip_configuration.network_configuration + ) + local_projects = [] # type: List[LocalProjectRequirement] + for direct_requirement in direct_requirements: + if isinstance(direct_requirement, LocalProjectRequirement): + local_projects.append(direct_requirement) + + source_paths = [local_project.path for local_project in local_projects] + list( + sdists + ) # type: List[str] + with TRACER.timed("Fingerprinting pre-resolved wheels"): + fingerprinted_wheels = tuple( + FingerprintedDistribution( + distribution=Distribution.load(dist_path), fingerprint=fingerprint + ) + for dist_path, fingerprint in iter_map_parallel( + inputs=wheels, + function=_fingerprint_dist, + max_jobs=pip_configuration.max_jobs, + costing_function=lambda whl: os.path.getsize(whl), + noun="wheel", + verb="fingerprint", + verb_past="fingerprinted", + ) + ) + + resolved_dists = [] # type: List[ResolvedDistribution] + resolve_installed_wheel_chroots = ( + fingerprinted_wheels and InstallableType.INSTALLED_WHEEL_CHROOT is result_type + ) + with TRACER.timed("Preparing pre-resolved distributions"): + if source_paths or resolve_installed_wheel_chroots: + package_index_configuration = PackageIndexConfiguration.create( + pip_version=pip_configuration.version, + resolver_version=pip_configuration.resolver_version, + indexes=pip_configuration.repos_configuration.indexes, + find_links=pip_configuration.repos_configuration.find_links, + network_configuration=pip_configuration.network_configuration, + password_entries=pip_configuration.repos_configuration.password_entries, + use_pip_config=pip_configuration.use_pip_config, + extra_pip_requirements=pip_configuration.extra_requirements, + ) + build_and_install = BuildAndInstallRequest( + build_requests=[ + BuildRequest.create(target=target, source_path=source_path) + for source_path in source_paths + for target in unique_targets + ], + install_requests=[ + InstallRequest( + target=target, wheel_path=wheel.location, fingerprint=wheel.fingerprint + ) + for wheel in fingerprinted_wheels + for target in unique_targets + ], + direct_requirements=direct_requirements, + package_index_configuration=package_index_configuration, + compile=compile, + build_configuration=pip_configuration.build_configuration, + verify_wheels=True, + pip_version=pip_configuration.version, + resolver=ConfiguredResolver(pip_configuration=pip_configuration), + dependency_configuration=dependency_configuration, + ) + resolved_dists.extend( + build_and_install.install_distributions( + ignore_errors=ignore_errors, + max_parallel_jobs=pip_configuration.max_jobs, + ) + if resolve_installed_wheel_chroots + else build_and_install.build_distributions( + ignore_errors=ignore_errors, + max_parallel_jobs=pip_configuration.max_jobs, + ) + ) + elif wheels: + direct_reqs_by_project_name = defaultdict( + list + ) # type: DefaultDict[ProjectName, List[Requirement]] + for parsed_req in direct_requirements: + assert not isinstance(parsed_req, LocalProjectRequirement) + direct_reqs_by_project_name[parsed_req.requirement.project_name].append( + parsed_req.requirement + ) + for wheel in fingerprinted_wheels: + direct_reqs = sorted_requirements(direct_reqs_by_project_name[wheel.project_name]) + for target in unique_targets: + resolved_dists.append( + ResolvedDistribution( + target=target, + fingerprinted_distribution=wheel, + direct_requirements=direct_reqs, + ) + ) + if not ignore_errors: + check_resolve(dependency_configuration, resolved_dists) + + with TRACER.timed("Sub-setting pre-resolved wheels"): + root_requirements = OrderedSet() # type: OrderedSet[Requirement] + locked_requirements = [] # type: List[LockedRequirement] + resolved_dist_by_file_artifact = {} # type: Dict[Artifact, ResolvedDistribution] + for resolved_dist in resolved_dists: + file_artifact = FileArtifact( + url=ArtifactURL.parse(resolved_dist.distribution.location), + fingerprint=Fingerprint(algorithm="sha256", hash=resolved_dist.fingerprint), + verified=True, + filename=os.path.basename(resolved_dist.distribution.location), + ) + dist_metadata = resolved_dist.distribution.metadata + locked_requirements.append( + LockedRequirement.create( + pin=Pin( + project_name=dist_metadata.project_name, + version=dist_metadata.version, + ), + artifact=file_artifact, + requires_python=dist_metadata.requires_python, + requires_dists=dist_metadata.requires_dists, + ) + ) + root_requirements.update(resolved_dist.direct_requirements) + resolved_dist_by_file_artifact[file_artifact] = resolved_dist + locked_resolve = LockedResolve( + locked_requirements=SortedTuple(locked_requirements), + platform_tag=PythonInterpreter.get().platform.tag, + ) # type: LockedResolve + + resolved_dists_subset = OrderedSet() # type: OrderedSet[ResolvedDistribution] + for target in unique_targets: + resolved = try_( + locked_resolve.resolve( + target=target, + requirements=root_requirements, + constraints=[ + constraint.requirement + for constraint in requirement_configuration.parse_constraints( + pip_configuration.network_configuration + ) + ], + transitive=True, + build_configuration=pip_configuration.build_configuration, + include_all_matches=False, + dependency_configuration=dependency_configuration, + ) + ) + for artifact in resolved.downloadable_artifacts: + resolved_dists_subset.add(resolved_dist_by_file_artifact[artifact.artifact]) + + return ResolveResult( + dependency_configuration=dependency_configuration, + distributions=tuple(resolved_dists_subset), + type=result_type, + ) diff --git a/pex/resolve/resolved_requirement.py b/pex/resolve/resolved_requirement.py index 5d8546055..eb63daa6c 100644 --- a/pex/resolve/resolved_requirement.py +++ b/pex/resolve/resolved_requirement.py @@ -7,7 +7,7 @@ from pex import hashing from pex.compatibility import url_unquote, urlparse -from pex.dist_metadata import ProjectNameAndVersion, Requirement +from pex.dist_metadata import ProjectNameAndVersion, Requirement, is_wheel from pex.hashing import HashlibHasher from pex.pep_440 import Version from pex.pep_503 import ProjectName @@ -91,7 +91,7 @@ class ArtifactURL(object): def parse(cls, url): # type: (str) -> ArtifactURL url_info = urlparse.urlparse(url) - scheme = parse_scheme(url_info.scheme) if url_info.scheme else None + scheme = parse_scheme(url_info.scheme) if url_info.scheme else "file" path = url_unquote(url_info.path) fingerprints = [] @@ -139,7 +139,7 @@ def parse(cls, url): raw_url = attr.ib(eq=False) # type: str download_url = attr.ib(eq=False) # type: str normalized_url = attr.ib() # type: str - scheme = attr.ib(eq=False) # type: Optional[Union[str, ArchiveScheme.Value, VCSScheme]] + scheme = attr.ib(eq=False) # type: Union[str, ArchiveScheme.Value, VCSScheme] path = attr.ib(eq=False) # type: str fragment_parameters = attr.ib(eq=False) # type: Mapping[str, Sequence[str]] fingerprints = attr.ib(eq=False) # type: Tuple[Fingerprint, ...] @@ -147,7 +147,7 @@ def parse(cls, url): @property def is_wheel(self): # type: () -> bool - return self.path.endswith(".whl") + return is_wheel(self.path) @property def fingerprint(self): diff --git a/pex/resolve/resolver_configuration.py b/pex/resolve/resolver_configuration.py index 61f788fcd..66f7983d2 100644 --- a/pex/resolve/resolver_configuration.py +++ b/pex/resolve/resolver_configuration.py @@ -220,3 +220,20 @@ def repos_configuration(self): def network_configuration(self): # type: () -> NetworkConfiguration return self.pip_configuration.network_configuration + + +@attr.s(frozen=True) +class PreResolvedConfiguration(object): + sdists = attr.ib() # type: Tuple[str, ...] + wheels = attr.ib() # type: Tuple[str, ...] + pip_configuration = attr.ib() # type: PipConfiguration + + @property + def repos_configuration(self): + # type: () -> ReposConfiguration + return self.pip_configuration.repos_configuration + + @property + def network_configuration(self): + # type: () -> NetworkConfiguration + return self.pip_configuration.network_configuration diff --git a/pex/resolve/resolver_options.py b/pex/resolve/resolver_options.py index 7ff37888d..967e43fd7 100644 --- a/pex/resolve/resolver_options.py +++ b/pex/resolve/resolver_options.py @@ -3,12 +3,13 @@ from __future__ import absolute_import +import glob import os from argparse import Action, ArgumentTypeError, Namespace, _ActionsContainer from pex import pex_warnings from pex.argparse import HandleBoolAction -from pex.dist_metadata import Requirement +from pex.dist_metadata import Requirement, is_sdist, is_wheel from pex.fetcher import initialize_ssl_context from pex.network_configuration import NetworkConfiguration from pex.orderedset import OrderedSet @@ -23,6 +24,7 @@ LockRepositoryConfiguration, PexRepositoryConfiguration, PipConfiguration, + PreResolvedConfiguration, ReposConfiguration, ResolverVersion, ) @@ -31,7 +33,7 @@ from pex.typing import TYPE_CHECKING, cast if TYPE_CHECKING: - from typing import Optional, Union + from typing import List, Optional, Union class _ManylinuxAction(Action): @@ -64,6 +66,7 @@ def register( parser, # type: _ActionsContainer include_pex_repository=False, # type: bool include_lock=False, # type: bool + include_pre_resolved=False, # type: bool ): # type: (...) -> None """Register resolver configuration options with the given parser. @@ -71,6 +74,8 @@ def register( :param parser: The parser to register resolver configuration options with. :param include_pex_repository: Whether to include the `--pex-repository` option. :param include_lock: Whether to include the `--lock` option. + :param include_pre_resolved: Whether to include the `--pre-resolved-dist` and + `--pre-resolved-dists` options. """ default_resolver_configuration = PipConfiguration() @@ -151,9 +156,15 @@ def register( help="Deprecated: No longer used.", ) - repository_choice = ( - parser.add_mutually_exclusive_group() if include_pex_repository and include_lock else parser - ) + repository_types = 0 + if include_pex_repository: + repository_types += 1 + if include_lock: + repository_types += 1 + if include_pre_resolved: + repository_types += 1 + + repository_choice = parser.add_mutually_exclusive_group() if repository_types > 1 else parser if include_pex_repository: repository_choice.add_argument( "--pex-repository", @@ -180,6 +191,23 @@ def register( ), ) register_lock_options(parser) + if include_pre_resolved: + repository_choice.add_argument( + "--pre-resolved-dist", + "--pre-resolved-dists", + dest="pre_resolved_dists", + metavar="FILE", + default=[], + type=str, + action="append", + help=( + "If a wheel, add it to the PEX. If an sdist, build wheels for the selected targets " + "and add them to the PEX. Otherwise, if a directory, add all the distributions " + "found in the given directory to the PEX, building wheels from any sdists first. " + "This option can be used to add a pre-resolved dependency set to a PEX. By " + "default, Pex will ensure the dependencies added form a closure." + ), + ) parser.add_argument( "--pre", @@ -481,7 +509,10 @@ class InvalidConfigurationError(Exception): if TYPE_CHECKING: ResolverConfiguration = Union[ - LockRepositoryConfiguration, PexRepositoryConfiguration, PipConfiguration + LockRepositoryConfiguration, + PexRepositoryConfiguration, + PipConfiguration, + PreResolvedConfiguration, ] @@ -494,26 +525,49 @@ def configure(options): """ pex_repository = getattr(options, "pex_repository", None) - lock = getattr(options, "lock", None) - if pex_repository and (options.indexes or options.find_links): - raise InvalidConfigurationError( - 'The "--pex-repository" option cannot be used together with the "--index" or ' - '"--find-links" options.' - ) - if pex_repository: + if options.indexes or options.find_links: + raise InvalidConfigurationError( + 'The "--pex-repository" option cannot be used together with the "--index" or ' + '"--find-links" options.' + ) return PexRepositoryConfiguration( pex_repository=pex_repository, network_configuration=create_network_configuration(options), transitive=options.transitive, ) + pip_configuration = create_pip_configuration(options) + lock = getattr(options, "lock", None) if lock: return LockRepositoryConfiguration( parse_lock=lambda: parse_lockfile(options, lock_file_path=lock), lock_file_path=lock, pip_configuration=pip_configuration, ) + + pre_resolved_dists = getattr(options, "pre_resolved_dists", None) + if pre_resolved_dists: + sdists = [] # type: List[str] + wheels = [] # type: List[str] + for dist_or_dir in pre_resolved_dists: + abs_dist_or_dir = os.path.expanduser(dist_or_dir) + dists = ( + [abs_dist_or_dir] + if os.path.isfile(abs_dist_or_dir) + else glob.glob(os.path.join(abs_dist_or_dir, "*")) + ) + for dist in dists: + if not os.path.isfile(dist): + continue + if is_wheel(dist): + wheels.append(dist) + elif is_sdist(dist): + sdists.append(dist) + return PreResolvedConfiguration( + sdists=tuple(sdists), wheels=tuple(wheels), pip_configuration=pip_configuration + ) + return pip_configuration diff --git a/pex/resolve/resolvers.py b/pex/resolve/resolvers.py index a2e12ebdb..68f679238 100644 --- a/pex/resolve/resolvers.py +++ b/pex/resolve/resolvers.py @@ -3,12 +3,17 @@ from __future__ import absolute_import +import itertools +import os from abc import abstractmethod +from collections import OrderedDict, defaultdict +from pex.common import pluralize from pex.dependency_configuration import DependencyConfiguration from pex.dist_metadata import Distribution, Requirement from pex.fingerprinted_distribution import FingerprintedDistribution from pex.pep_427 import InstallableType +from pex.pep_503 import ProjectName from pex.pip.version import PipVersionValue from pex.resolve.lockfile.model import Lockfile from pex.sorted_tuple import SortedTuple @@ -16,7 +21,7 @@ from pex.typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Iterable, Optional, Tuple + from typing import DefaultDict, Iterable, List, Optional, Tuple import attr # vendor:skip else: @@ -81,6 +86,90 @@ def with_direct_requirements(self, direct_requirements=None): ) +def check_resolve( + dependency_configuration, # type: DependencyConfiguration + resolved_distributions, # type: Iterable[ResolvedDistribution] +): + # type: (...) -> None + resolved_distributions_by_project_name = ( + OrderedDict() + ) # type: OrderedDict[ProjectName, List[ResolvedDistribution]] + for resolved_distribution in resolved_distributions: + resolved_distributions_by_project_name.setdefault( + resolved_distribution.distribution.metadata.project_name, [] + ).append(resolved_distribution) + + unsatisfied = defaultdict(list) # type: DefaultDict[Target, List[str]] + for resolved_distribution in itertools.chain.from_iterable( + resolved_distributions_by_project_name.values() + ): + dist = resolved_distribution.distribution + target = resolved_distribution.target + + for requirement in dist.requires(): + if dependency_configuration.excluded_by(requirement): + continue + requirement = ( + dependency_configuration.overridden_by(requirement, target=target) or requirement + ) + if not target.requirement_applies(requirement): + continue + + installed_requirement_dists = resolved_distributions_by_project_name.get( + requirement.project_name + ) + if not installed_requirement_dists: + unsatisfied[target].append( + "{dist} requires {requirement} but no version was resolved".format( + dist=dist.as_requirement(), requirement=requirement + ) + ) + else: + resolved_dists = [ + installed_requirement_dist.distribution + for installed_requirement_dist in installed_requirement_dists + ] + if not any( + ( + requirement.specifier.contains(resolved_dist.version, prereleases=True) + and target.wheel_applies(resolved_dist) + ) + for resolved_dist in resolved_dists + ): + unsatisfied[target].append( + "{dist} requires {requirement} but {count} incompatible {dists_were} " + "resolved:\n {dists}".format( + dist=dist, + requirement=requirement, + count=len(resolved_dists), + dists_were="dists were" if len(resolved_dists) > 1 else "dist was", + dists="\n ".join( + os.path.basename(resolved_dist.location) + for resolved_dist in resolved_dists + ), + ) + ) + + if unsatisfied: + unsatisfieds = [] + for target, missing in unsatisfied.items(): + unsatisfieds.append( + "{target} is not compatible with:\n {missing}".format( + target=target.render_description(), missing="\n ".join(missing) + ) + ) + raise Unsatisfiable( + "Failed to resolve compatible distributions for {count} {targets}:\n{failures}".format( + count=len(unsatisfieds), + targets=pluralize(unsatisfieds, "target"), + failures="\n".join( + "{index}: {failure}".format(index=index, failure=failure) + for index, failure in enumerate(unsatisfieds, start=1) + ), + ) + ) + + @attr.s(frozen=True) class ResolveResult(object): dependency_configuration = attr.ib() # type: DependencyConfiguration diff --git a/pex/resolver.py b/pex/resolver.py index 263d24370..47e495bd2 100644 --- a/pex/resolver.py +++ b/pex/resolver.py @@ -20,13 +20,12 @@ from pex.common import pluralize, safe_mkdir, safe_mkdtemp from pex.compatibility import url_unquote, urlparse from pex.dependency_configuration import DependencyConfiguration -from pex.dist_metadata import DistMetadata, Distribution, ProjectNameAndVersion, Requirement +from pex.dist_metadata import DistMetadata, Distribution, Requirement, is_wheel from pex.fingerprinted_distribution import FingerprintedDistribution from pex.jobs import Raise, SpawnedJob, execute_parallel, iter_map_parallel from pex.network_configuration import NetworkConfiguration from pex.orderedset import OrderedSet from pex.pep_376 import InstalledWheel -from pex.pep_425 import CompatibilityTags from pex.pep_427 import InstallableType, WheelError, install_wheel_chroot from pex.pep_503 import ProjectName from pex.pip.download_observer import DownloadObserver @@ -43,6 +42,7 @@ ResolveResult, Unsatisfiable, Untranslatable, + check_resolve, ) from pex.targets import LocalInterpreter, Target, Targets from pex.tracer import TRACER @@ -170,7 +170,7 @@ class DownloadResult(object): @staticmethod def _is_wheel(path): # type: (str) -> bool - return os.path.isfile(path) and path.endswith(".whl") + return is_wheel(path) and zipfile.is_zipfile(path) target = attr.ib() # type: Target download_dir = attr.ib() # type: str @@ -318,9 +318,8 @@ def finalize_build(self, check_compatible=True): ) wheel_path = wheels[0] if check_compatible and self.request.target.is_foreign: - wheel_tags = CompatibilityTags.from_wheel(wheel_path) - if not self.request.target.supported_tags.compatible_tags(wheel_tags): - project_name_and_version = ProjectNameAndVersion.from_filename(wheel_path) + wheel = Distribution.load(wheel_path) + if not self.request.target.wheel_applies(wheel): raise ValueError( "No pre-built wheel was available for {project_name} {version}.{eol}" "Successfully built the wheel {wheel} from the sdist {sdist} but it is not " @@ -328,8 +327,8 @@ def finalize_build(self, check_compatible=True): "You'll need to build a wheel from {sdist} on the foreign target platform and " "make it available to Pex via a `--find-links` repo or a custom " "`--index`.".format( - project_name=project_name_and_version.project_name, - version=project_name_and_version.version, + project_name=wheel.project_name, + version=wheel.version, eol=os.linesep, wheel=os.path.basename(wheel_path), sdist=os.path.basename(self.request.source_path), @@ -789,7 +788,7 @@ def _resolve_direct_file_deps( "The {wheel} wheel has a dependency on {url} which does not exist on this " "machine.".format(wheel=install_request.wheel_file, url=requirement.url) ) - if dist_path.endswith(".whl"): + if is_wheel(dist_path): to_install.add(InstallRequest.create(install_request.target, dist_path)) else: to_build.add(BuildRequest.create(install_request.target, dist_path)) @@ -873,7 +872,7 @@ def build_distributions( if not ignore_errors: with TRACER.timed("Checking build"): - self._check(wheels) + check_resolve(self._dependency_configuration, wheels) return direct_requirements.adjust(wheels) def install_distributions( @@ -946,77 +945,9 @@ def add_installation(install_result): if not ignore_errors: with TRACER.timed("Checking install"): - self._check(installations) + check_resolve(self._dependency_configuration, installations) return direct_requirements.adjust(installations) - def _check(self, resolved_distributions): - # type: (Iterable[ResolvedDistribution]) -> None - resolved_distributions_by_project_name = ( - OrderedDict() - ) # type: OrderedDict[ProjectName, List[ResolvedDistribution]] - for resolved_distribution in resolved_distributions: - resolved_distributions_by_project_name.setdefault( - resolved_distribution.distribution.metadata.project_name, [] - ).append(resolved_distribution) - - unsatisfied = [] - for resolved_distribution in itertools.chain.from_iterable( - resolved_distributions_by_project_name.values() - ): - dist = resolved_distribution.distribution - target = resolved_distribution.target - for requirement in dist.requires(): - if self._dependency_configuration.excluded_by(requirement): - continue - requirement = ( - self._dependency_configuration.overridden_by(requirement, target=target) - or requirement - ) - if not target.requirement_applies(requirement): - continue - - installed_requirement_dists = resolved_distributions_by_project_name.get( - requirement.project_name - ) - if not installed_requirement_dists: - unsatisfied.append( - "{dist} requires {requirement} but no version was resolved".format( - dist=dist.as_requirement(), requirement=requirement - ) - ) - else: - resolved_dists = [ - installed_requirement_dist.distribution - for installed_requirement_dist in installed_requirement_dists - ] - if not any( - requirement.specifier.contains(resolved_dist.version, prereleases=True) - for resolved_dist in resolved_dists - ): - unsatisfied.append( - "{dist} requires {requirement} but {count} incompatible {dists_were} " - "resolved: {dists}".format( - dist=dist.as_requirement(), - requirement=requirement, - count=len(resolved_dists), - dists_were="dists were" if len(resolved_dists) > 1 else "dist was", - dists=" ".join( - os.path.basename(resolved_dist.location) - for resolved_dist in resolved_dists - ), - ) - ) - - if unsatisfied: - raise Unsatisfiable( - "Failed to resolve compatible distributions:\n{failures}".format( - failures="\n".join( - "{index}: {failure}".format(index=index, failure=failure) - for index, failure in enumerate(unsatisfied, start=1) - ) - ) - ) - def _parse_reqs( requirements=None, # type: Optional[Iterable[str]] @@ -1265,7 +1196,7 @@ def _calculate_fingerprint(self): @property def is_wheel(self): - return self.path.endswith(".whl") and zipfile.is_zipfile(self.path) + return is_wheel(self.path) and zipfile.is_zipfile(self.path) @attr.s(frozen=True) diff --git a/pex/targets.py b/pex/targets.py index 37c02f229..49ad956f5 100644 --- a/pex/targets.py +++ b/pex/targets.py @@ -5,14 +5,15 @@ import os -from pex.dist_metadata import Requirement +from pex.dist_metadata import Distribution, Requirement from pex.interpreter import PythonInterpreter, calculate_binary_name from pex.orderedset import OrderedSet -from pex.pep_425 import CompatibilityTags +from pex.pep_425 import CompatibilityTags, RankedTag from pex.pep_508 import MarkerEnvironment from pex.platforms import Platform from pex.result import Error from pex.third_party.packaging.specifiers import SpecifierSet +from pex.third_party.packaging.tags import Tag from pex.typing import TYPE_CHECKING, cast if TYPE_CHECKING: @@ -27,6 +28,21 @@ class RequiresPythonError(Exception): """Indicates the impossibility of evaluating Requires-Python metadata.""" +@attr.s(frozen=True) +class WheelEvaluation(object): + tags = attr.ib() # type: Tuple[Tag, ...] + best_match = attr.ib() # type: Optional[RankedTag] + requires_python = attr.ib() # type: Optional[SpecifierSet] + applies = attr.ib() # type: bool + + def __bool__(self): + # type: () -> bool + return self.applies + + # N.B.: For Python 2.7. + __nonzero__ = __bool__ + + @attr.s(frozen=True) class Target(object): id = attr.ib() # type: str @@ -153,6 +169,25 @@ def requirement_applies( return False + def wheel_applies(self, wheel): + # type: (Distribution) -> WheelEvaluation + wheel_tags = CompatibilityTags.from_wheel(wheel.location) + ranked_tag = self.supported_tags.best_match(wheel_tags) + return WheelEvaluation( + tags=tuple(wheel_tags), + best_match=ranked_tag, + requires_python=wheel.metadata.requires_python, + applies=( + ranked_tag is not None + and ( + not wheel.metadata.requires_python + or self.requires_python_applies( + wheel.metadata.requires_python, source=wheel.location + ) + ) + ), + ) + def __str__(self): # type: () -> str return str(self.platform.tag) diff --git a/pex/version.py b/pex/version.py index 5183b0cb7..95550c4e8 100644 --- a/pex/version.py +++ b/pex/version.py @@ -1,4 +1,4 @@ # Copyright 2015 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -__version__ = "2.18.1" +__version__ = "2.19.0" diff --git a/testing/data/locks/devpi-server.lock.json b/testing/data/locks/devpi-server.lock.json new file mode 100644 index 000000000..377bbad82 --- /dev/null +++ b/testing/data/locks/devpi-server.lock.json @@ -0,0 +1,1794 @@ +{ + "allow_builds": true, + "allow_prereleases": false, + "allow_wheels": true, + "build_isolation": true, + "constraints": [], + "excluded": [], + "locked_resolves": [ + { + "locked_requirements": [ + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", + "url": "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", + "url": "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz" + } + ], + "project_name": "anyio", + "requires_dists": [ + "Sphinx>=7; extra == \"doc\"", + "anyio[trio]; extra == \"test\"", + "coverage[toml]>=7; extra == \"test\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "exceptiongroup>=1.2.0; extra == \"test\"", + "hypothesis>=4.0; extra == \"test\"", + "idna>=2.8", + "packaging; extra == \"doc\"", + "psutil>=5.9; extra == \"test\"", + "pytest-mock>=3.6.1; extra == \"test\"", + "pytest>=7.0; extra == \"test\"", + "sniffio>=1.1", + "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", + "sphinx-rtd-theme; extra == \"doc\"", + "trio>=0.23; extra == \"trio\"", + "trustme; extra == \"test\"", + "typing-extensions>=4.1; python_version < \"3.11\"", + "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" + ], + "requires_python": ">=3.8", + "version": "4.4.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", + "url": "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", + "url": "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz" + } + ], + "project_name": "argon2-cffi", + "requires_dists": [ + "argon2-cffi-bindings", + "argon2-cffi[tests,typing]; extra == \"dev\"", + "furo; extra == \"docs\"", + "hypothesis; extra == \"tests\"", + "mypy; extra == \"typing\"", + "myst-parser; extra == \"docs\"", + "pytest; extra == \"tests\"", + "sphinx-copybutton; extra == \"docs\"", + "sphinx-notfound-page; extra == \"docs\"", + "sphinx; extra == \"docs\"", + "tox>4; extra == \"dev\"", + "typing-extensions; python_version < \"3.8\"" + ], + "requires_python": ">=3.7", + "version": "23.1.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", + "url": "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", + "url": "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", + "url": "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", + "url": "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", + "url": "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", + "url": "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", + "url": "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", + "url": "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", + "url": "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + } + ], + "project_name": "argon2-cffi-bindings", + "requires_dists": [ + "cffi>=1.0.1", + "cogapp; extra == \"dev\"", + "pre-commit; extra == \"dev\"", + "pytest; extra == \"dev\"", + "pytest; extra == \"tests\"", + "wheel; extra == \"dev\"" + ], + "requires_python": ">=3.6", + "version": "21.2.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", + "url": "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "url": "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz" + } + ], + "project_name": "attrs", + "requires_dists": [ + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"benchmark\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"cov\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"dev\"", + "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"tests\"", + "cogapp; extra == \"docs\"", + "coverage[toml]>=5.3; extra == \"cov\"", + "furo; extra == \"docs\"", + "hypothesis; extra == \"benchmark\"", + "hypothesis; extra == \"cov\"", + "hypothesis; extra == \"dev\"", + "hypothesis; extra == \"tests\"", + "importlib-metadata; python_version < \"3.8\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"benchmark\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"cov\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"dev\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests\"", + "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests-mypy\"", + "myst-parser; extra == \"docs\"", + "pre-commit; extra == \"dev\"", + "pympler; extra == \"benchmark\"", + "pympler; extra == \"cov\"", + "pympler; extra == \"dev\"", + "pympler; extra == \"tests\"", + "pytest-codspeed; extra == \"benchmark\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"benchmark\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"cov\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"dev\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests-mypy\"", + "pytest-xdist[psutil]; extra == \"benchmark\"", + "pytest-xdist[psutil]; extra == \"cov\"", + "pytest-xdist[psutil]; extra == \"dev\"", + "pytest-xdist[psutil]; extra == \"tests\"", + "pytest>=4.3.0; extra == \"benchmark\"", + "pytest>=4.3.0; extra == \"cov\"", + "pytest>=4.3.0; extra == \"dev\"", + "pytest>=4.3.0; extra == \"tests\"", + "sphinx-notfound-page; extra == \"docs\"", + "sphinx; extra == \"docs\"", + "sphinxcontrib-towncrier; extra == \"docs\"", + "towncrier<24.7; extra == \"docs\"" + ], + "requires_python": ">=3.7", + "version": "24.2.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "url": "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", + "url": "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz" + } + ], + "project_name": "certifi", + "requires_dists": [], + "requires_python": ">=3.6", + "version": "2024.8.30" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "url": "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "url": "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "url": "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "url": "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "url": "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "url": "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "url": "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "url": "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "url": "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "url": "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "url": "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "url": "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "url": "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "url": "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "url": "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "url": "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "url": "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "url": "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "url": "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "url": "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "url": "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "url": "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "url": "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "url": "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "url": "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "url": "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "url": "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "url": "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "url": "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "url": "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "url": "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "url": "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "url": "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "url": "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "url": "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "url": "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", + "url": "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "url": "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "url": "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + } + ], + "project_name": "cffi", + "requires_dists": [ + "pycparser" + ], + "requires_python": ">=3.8", + "version": "1.17.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "url": "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "url": "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "url": "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "url": "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "url": "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "url": "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "url": "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "url": "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "url": "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "url": "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "url": "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "url": "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "url": "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "url": "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "url": "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "url": "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "url": "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "url": "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "url": "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "url": "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "url": "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "url": "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "url": "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "url": "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "url": "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "url": "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "url": "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "url": "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "url": "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "url": "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "url": "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "url": "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "url": "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "url": "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "url": "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "url": "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "url": "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "url": "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "url": "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "url": "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "url": "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" + } + ], + "project_name": "charset-normalizer", + "requires_dists": [], + "requires_python": ">=3.7.0", + "version": "3.3.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", + "url": "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "url": "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz" + } + ], + "project_name": "defusedxml", + "requires_dists": [], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "version": "0.7.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "8a83b788ac821ade1e348d571db083575c5c5d92ba0932a3a8f75c6f8dde4d88", + "url": "https://files.pythonhosted.org/packages/7c/f8/923116cb379060431a8df6c6c561b78734e2fc9e933e0365e06f8622aaf4/devpi_common-4.0.4-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "235a0a9a45c96e54c60ba6ba2f77d856cf90f1a69c1bee949887e9edc03a41cc", + "url": "https://files.pythonhosted.org/packages/4e/9a/3522931638d784eae9c854a023b86b6455db289ca823ab6eb257af433d4a/devpi_common-4.0.4.tar.gz" + } + ], + "project_name": "devpi-common", + "requires_dists": [ + "lazy", + "packaging-legacy", + "requests>=2.3.0" + ], + "requires_python": ">=3.7", + "version": "4.0.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "1bf0859784441a902ec264754569ffb78fcac2250a923ba03948dd64b5f05283", + "url": "https://files.pythonhosted.org/packages/ec/2a/46707c8a8fb0c79c3e64a15ca71827f03e665beaf4ccc509f4f2bbd51c05/devpi_server-6.12.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "72bb14b545b2dda6f1b279da3885475b0eb3a763152442abcd41ce39219310c5", + "url": "https://files.pythonhosted.org/packages/c1/cd/722e084bc5dfb2a598d1dffb3fb178d6a50a2617bbdf5e4e49e7b369aa78/devpi-server-6.12.1.tar.gz" + } + ], + "project_name": "devpi-server", + "requires_dists": [ + "argon2-cffi", + "attrs>=22.2.0", + "defusedxml", + "devpi-common<5,>3.6.0", + "httpx", + "itsdangerous>=0.24", + "lazy", + "legacy-cgi; python_version >= \"3.13\"", + "passlib[argon2]", + "platformdirs", + "pluggy<2.0,>=0.6.0", + "py>=1.4.23", + "pyramid>=2", + "repoze.lru>=0.6", + "ruamel.yaml", + "strictyaml", + "waitress>=1.0.1" + ], + "requires_python": ">=3.7", + "version": "6.12.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "url": "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", + "url": "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz" + } + ], + "project_name": "exceptiongroup", + "requires_dists": [ + "pytest>=6; extra == \"test\"" + ], + "requires_python": ">=3.7", + "version": "1.2.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", + "url": "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "url": "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz" + } + ], + "project_name": "h11", + "requires_dists": [ + "typing-extensions; python_version < \"3.8\"" + ], + "requires_python": ">=3.7", + "version": "0.14.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", + "url": "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", + "url": "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz" + } + ], + "project_name": "httpcore", + "requires_dists": [ + "anyio<5.0,>=4.0; extra == \"asyncio\"", + "certifi", + "h11<0.15,>=0.13", + "h2<5,>=3; extra == \"http2\"", + "socksio==1.*; extra == \"socks\"", + "trio<0.26.0,>=0.22.0; extra == \"trio\"" + ], + "requires_python": ">=3.8", + "version": "1.0.5" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", + "url": "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", + "url": "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz" + } + ], + "project_name": "httpx", + "requires_dists": [ + "anyio", + "brotli; platform_python_implementation == \"CPython\" and extra == \"brotli\"", + "brotlicffi; platform_python_implementation != \"CPython\" and extra == \"brotli\"", + "certifi", + "click==8.*; extra == \"cli\"", + "h2<5,>=3; extra == \"http2\"", + "httpcore==1.*", + "idna", + "pygments==2.*; extra == \"cli\"", + "rich<14,>=10; extra == \"cli\"", + "sniffio", + "socksio==1.*; extra == \"socks\"", + "zstandard>=0.18.0; extra == \"zstd\"" + ], + "requires_python": ">=3.8", + "version": "0.27.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19", + "url": "https://files.pythonhosted.org/packages/86/7d/3888833e4f5ea56af4a9935066ec09a83228e533d7b8877f65889d706ee4/hupper-1.12.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb", + "url": "https://files.pythonhosted.org/packages/bd/e6/bb064537288eee2be97f3e0fcad8e7242bc5bbe9664ae57c7d29b3fa18c2/hupper-1.12.1.tar.gz" + } + ], + "project_name": "hupper", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "mock; extra == \"testing\"", + "pylons-sphinx-themes; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"", + "setuptools; extra == \"docs\"", + "watchdog; extra == \"docs\"", + "watchdog; extra == \"testing\"" + ], + "requires_python": ">=3.7", + "version": "1.12.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "url": "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", + "url": "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz" + } + ], + "project_name": "idna", + "requires_dists": [], + "requires_python": ">=3.6", + "version": "3.8" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "url": "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", + "url": "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz" + } + ], + "project_name": "itsdangerous", + "requires_dists": [], + "requires_python": ">=3.8", + "version": "2.2.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "449375c125c7acac6b7a93f71b8e7ccb06546c37b161613f92d2d3981f793244", + "url": "https://files.pythonhosted.org/packages/11/ae/3ae578fc22dc9c5f60ddcb5c254fe808d45ee7b4cd03315245caf5db6a47/lazy-1.6-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "7127324ec709e8324f08cb4611c1abe01776bda53bb9ce68dc5dfa46ca0ed3e9", + "url": "https://files.pythonhosted.org/packages/3c/e6/704c32da169b023a9ac86d116f5433b42d02b4afeda24c9400a69b3530e5/lazy-1.6.tar.gz" + } + ], + "project_name": "lazy", + "requires_dists": [ + "mypy; extra == \"mypy\"", + "sphinx-rtd-theme==1.0.0; extra == \"docs\"", + "sphinx==5.3.0; extra == \"docs\"" + ], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7", + "version": "1.6" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "8eacc1522d9f76451337a4b5a0abf494158d39250754b0d1bc19a14c6512af9b", + "url": "https://files.pythonhosted.org/packages/9b/da/4cbc703cccc326bac1b4311609e694729134d1e8a2b45c224f7cb2602590/legacy_cgi-2.6.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f2ada99c747c3d72a473a6aaff6259a61f226b06fe9f3106e495ab83fd8f7a42", + "url": "https://files.pythonhosted.org/packages/48/96/ff14ad0f759f2297a2e61db9c5384d248a6b38c6c1d4452c07d7419676a2/legacy_cgi-2.6.1.tar.gz" + } + ], + "project_name": "legacy-cgi", + "requires_dists": [], + "requires_python": "<4.0,>=3.10", + "version": "2.6.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", + "url": "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "url": "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz" + } + ], + "project_name": "packaging", + "requires_dists": [], + "requires_python": ">=3.8", + "version": "24.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "6cd21cd283c09409349bccc10bb55bfd837b4aab86a7b0f87bfcb8dd9831a8a3", + "url": "https://files.pythonhosted.org/packages/64/3a/cca5260a6e346027495775bef30869acce88d6f72f69ad7c27a0ba87ec95/packaging_legacy-23.0.post0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "c974a42291a77112313f0198b87ad96e07a3c357295d572560a4b9c368f7d9db", + "url": "https://files.pythonhosted.org/packages/f8/31/3a2fe3f5fc01a0671ba20560c556b4239b69bfef842a20bba99e3239fd3e/packaging_legacy-23.0.post0.tar.gz" + } + ], + "project_name": "packaging-legacy", + "requires_dists": [ + "packaging>=23.0" + ], + "requires_python": null, + "version": "23.0.post0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", + "url": "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", + "url": "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz" + } + ], + "project_name": "passlib", + "requires_dists": [ + "argon2-cffi>=18.2.0; extra == \"argon2\"", + "bcrypt>=3.1.0; extra == \"bcrypt\"", + "cloud-sptheme>=1.10.1; extra == \"build-docs\"", + "cryptography; extra == \"totp\"", + "sphinx>=1.6; extra == \"build-docs\"", + "sphinxcontrib-fulltoc>=1.2.0; extra == \"build-docs\"" + ], + "requires_python": null, + "version": "1.7.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "76388ad53a661448d436df28c798063108f70e994ddc749540d733cdbd1b38cf", + "url": "https://files.pythonhosted.org/packages/85/30/cdddd9a88969683a59222a6d61cd6dce923977f2e9f9ffba38e1324149cd/PasteDeploy-3.1.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "9ddbaf152f8095438a9fe81f82c78a6714b92ae8e066bed418b6a7ff6a095a95", + "url": "https://files.pythonhosted.org/packages/e3/97/0c4a613ec96a54d21daa7e089178263915554320402e89b4e319436a63cb/PasteDeploy-3.1.0.tar.gz" + } + ], + "project_name": "pastedeploy", + "requires_dists": [ + "Paste; extra == \"paste\"", + "Paste; extra == \"testing\"", + "Sphinx>=1.7.5; extra == \"docs\"", + "importlib-metadata; python_version < \"3.8\"", + "pylons-sphinx-themes; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"" + ], + "requires_python": ">=3.7", + "version": "3.1.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "42992ab1f4865f1278e2ad740e8ad145683bb4022e03534265528f0c23c0df2d", + "url": "https://files.pythonhosted.org/packages/e7/8b/3f98db1448e3b4d2d142716874a7e02f6101685fdaa0f55a8668e9ffa048/plaster-1.1.2-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f8befc54bf8c1147c10ab40297ec84c2676fa2d4ea5d6f524d9436a80074ef98", + "url": "https://files.pythonhosted.org/packages/26/93/66df0f87f1442d8afea8531ae8a4a9eca656006a54eac2b4489427e92c10/plaster-1.1.2.tar.gz" + } + ], + "project_name": "plaster", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "importlib-metadata; python_version < \"3.8\"", + "pylons-sphinx-themes; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"" + ], + "requires_python": ">=3.7", + "version": "1.1.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "ad3550cc744648969ed3b810f33c9344f515ee8d8a8cec18e8f2c4a643c2181f", + "url": "https://files.pythonhosted.org/packages/bd/30/2d4cf89035c22a89bf0e34dbc50fdc07c42c9bdc90fd972d495257ad2b6e/plaster_pastedeploy-1.0.1-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "be262e6d2e41a7264875daa2fe2850cbb0615728bcdc92828fdc72736e381412", + "url": "https://files.pythonhosted.org/packages/c7/af/01a22f73ce97c6375c88d7ceaf6f5f4f345e940da93c94f98833d898a449/plaster_pastedeploy-1.0.1.tar.gz" + } + ], + "project_name": "plaster-pastedeploy", + "requires_dists": [ + "PasteDeploy>=2.0", + "plaster>=0.5", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"" + ], + "requires_python": ">=3.7", + "version": "1.0.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617", + "url": "https://files.pythonhosted.org/packages/da/8b/d497999c4017b80678017ddce745cf675489c110681ad3c84a55eddfd3e7/platformdirs-4.3.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", + "url": "https://files.pythonhosted.org/packages/75/a0/d7cab8409cdc7d39b037c85ac46d92434fb6595432e069251b38e5c8dd0e/platformdirs-4.3.2.tar.gz" + } + ], + "project_name": "platformdirs", + "requires_dists": [ + "appdirs==1.4.4; extra == \"test\"", + "covdefaults>=2.3; extra == \"test\"", + "furo>=2024.8.6; extra == \"docs\"", + "mypy>=1.11.2; extra == \"type\"", + "proselint>=0.14; extra == \"docs\"", + "pytest-cov>=5; extra == \"test\"", + "pytest-mock>=3.14; extra == \"test\"", + "pytest>=8.3.2; extra == \"test\"", + "sphinx-autodoc-typehints>=2.4; extra == \"docs\"", + "sphinx>=8.0.2; extra == \"docs\"" + ], + "requires_python": ">=3.8", + "version": "4.3.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", + "url": "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "url": "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz" + } + ], + "project_name": "pluggy", + "requires_dists": [ + "pre-commit; extra == \"dev\"", + "pytest-benchmark; extra == \"testing\"", + "pytest; extra == \"testing\"", + "tox; extra == \"dev\"" + ], + "requires_python": ">=3.8", + "version": "1.5.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", + "url": "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "url": "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz" + } + ], + "project_name": "py", + "requires_dists": [], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", + "version": "1.11.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", + "url": "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "url": "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" + } + ], + "project_name": "pycparser", + "requires_dists": [], + "requires_python": ">=3.8", + "version": "2.22" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2e6585ac55c147f0a51bc00dadf72075b3bdd9a871b332ff9e5e04117ccd76fa", + "url": "https://files.pythonhosted.org/packages/db/41/a2114b8dd2187ae007e022a2baabdc7937cc78211cefc0c01fc5452193af/pyramid-2.0.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "372138a738e4216535cc76dcce6eddd5a1aaca95130f2354fb834264c06f18de", + "url": "https://files.pythonhosted.org/packages/47/c3/5d5736e692fc7ff052577f03136b5edfdf1e2e177eff2f4b91206acae11d/pyramid-2.0.2.tar.gz" + } + ], + "project_name": "pyramid", + "requires_dists": [ + "Sphinx>=3.0.0; extra == \"docs\"", + "coverage; extra == \"testing\"", + "docutils; extra == \"docs\"", + "hupper>=1.5", + "plaster", + "plaster-pastedeploy", + "pylons-sphinx-latesturl; extra == \"docs\"", + "pylons-sphinx-themes>=1.0.8; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest>=5.4.2; extra == \"testing\"", + "repoze.sphinx.autointerface; extra == \"docs\"", + "setuptools", + "sphinx-copybutton; extra == \"docs\"", + "sphinxcontrib-autoprogram; extra == \"docs\"", + "translationstring>=0.4", + "venusian>=1.0", + "webob>=1.8.3", + "webtest>=1.3.1; extra == \"testing\"", + "zope.component>=4.0; extra == \"testing\"", + "zope.deprecation>=3.5.0", + "zope.interface>=3.8.0" + ], + "requires_python": ">=3.6", + "version": "2.0.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", + "url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "url": "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz" + } + ], + "project_name": "python-dateutil", + "requires_dists": [ + "six>=1.5" + ], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", + "version": "2.9.0.post0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "f77bf0e1096ea445beadd35f3479c5cff2aa1efe604a133e67150bc8630a62ea", + "url": "https://files.pythonhosted.org/packages/b0/30/6cc0c95f0b59ad4b3b9163bff7cdcf793cc96fac64cf398ff26271f5cf5e/repoze.lru-0.7-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "0429a75e19380e4ed50c0694e26ac8819b4ea7851ee1fc7583c8572db80aff77", + "url": "https://files.pythonhosted.org/packages/12/bc/595a77c4b5e204847fdf19268314ef59c85193a9dc9f83630fc459c0fee5/repoze.lru-0.7.tar.gz" + } + ], + "project_name": "repoze-lru", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "coverage; extra == \"testing\"", + "nose; extra == \"testing\"" + ], + "requires_python": null, + "version": "0.7" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", + "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz" + } + ], + "project_name": "requests", + "requires_dists": [ + "PySocks!=1.5.7,>=1.5.6; extra == \"socks\"", + "certifi>=2017.4.17", + "chardet<6,>=3.0.2; extra == \"use-chardet-on-py3\"", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1" + ], + "requires_python": ">=3.8", + "version": "2.32.3" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "url": "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", + "url": "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz" + } + ], + "project_name": "ruamel-yaml", + "requires_dists": [ + "mercurial>5.7; extra == \"docs\"", + "ruamel.yaml.clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", + "ruamel.yaml.jinja2>=0.2; extra == \"jinja2\"", + "ryd; extra == \"docs\"" + ], + "requires_python": ">=3.7", + "version": "0.18.6" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", + "url": "https://files.pythonhosted.org/packages/27/38/4cf4d482b84ecdf51efae6635cc5483a83cf5ca9d9c13e205a750e251696/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", + "url": "https://files.pythonhosted.org/packages/01/b0/4ddef56e9f703d7909febc3a421d709a3482cda25826816ec595b73e3847/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", + "url": "https://files.pythonhosted.org/packages/0d/aa/06db7ca0995b513538402e11280282c615b5ae5f09eb820460d35fb69715/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", + "url": "https://files.pythonhosted.org/packages/30/d3/5fe978cd01a61c12efd24d65fa68c6f28f28c8073a06cf11db3a854390ca/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", + "url": "https://files.pythonhosted.org/packages/46/ab/bab9eb1566cd16f060b54055dd39cf6a34bfa0240c53a7218c43e974295b/ruamel.yaml.clib-0.2.8.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", + "url": "https://files.pythonhosted.org/packages/55/b3/e2531a050758b717c969cbf76c103b75d8a01e11af931b94ba656117fbe9/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", + "url": "https://files.pythonhosted.org/packages/61/ee/4874c9fc96010fce85abefdcbe770650c5324288e988d7a48b527a423815/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", + "url": "https://files.pythonhosted.org/packages/66/98/8de4f22bbfd9135deb3422e96d450c4bc0a57d38c25976119307d2efe0aa/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", + "url": "https://files.pythonhosted.org/packages/7a/a2/eb5e9d088cb9d15c24d956944c09dca0a89108ad6e2e913c099ef36e3f0d/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", + "url": "https://files.pythonhosted.org/packages/7c/e4/0d19d65e340f93df1c47f323d95fa4b256bb28320290f5fddef90837853a/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", + "url": "https://files.pythonhosted.org/packages/88/30/fc45b45d5eaf2ff36cffd215a2f85e9b90ac04e70b97fd4097017abfb567/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", + "url": "https://files.pythonhosted.org/packages/90/8c/6cdb44f548b29eb6328b9e7e175696336bc856de2ff82e5776f860f03822/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", + "url": "https://files.pythonhosted.org/packages/a4/f7/22d6b620ed895a05d40802d8281eff924dc6190f682d933d4efff60db3b5/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", + "url": "https://files.pythonhosted.org/packages/af/dc/133547f90f744a0c827bac5411d84d4e81da640deb3af1459e38c5f3b6a0/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", + "url": "https://files.pythonhosted.org/packages/b1/15/971b385c098e8d0d170893f5ba558452bb7b776a0c90658b8f4dd0e3382b/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", + "url": "https://files.pythonhosted.org/packages/c9/ff/f781eb5e2ae011e586d5426e2086a011cf1e0f59704a6cad1387975c5a62/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", + "url": "https://files.pythonhosted.org/packages/ca/01/37ac131614f71b98e9b148b2d7790662dcee92217d2fb4bac1aa377def33/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412", + "url": "https://files.pythonhosted.org/packages/d3/62/c60b034d9a008bbd566eeecf53a5a4c73d191c8de261290db6761802b72d/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", + "url": "https://files.pythonhosted.org/packages/e3/41/f62e67ac651358b8f0d60cfb12ab2daf99b1b69eeaa188d0cec809d943a6/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl" + } + ], + "project_name": "ruamel-yaml-clib", + "requires_dists": [], + "requires_python": ">=3.6", + "version": "0.2.8" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", + "url": "https://files.pythonhosted.org/packages/cb/9c/9ad11ac06b97e55ada655f8a6bea9d1d3f06e120b178cd578d80e558191d/setuptools-74.1.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6", + "url": "https://files.pythonhosted.org/packages/3e/2c/f0a538a2f91ce633a78daaeb34cbfb93a54bd2132a6de1f6cec028eee6ef/setuptools-74.1.2.tar.gz" + } + ], + "project_name": "setuptools", + "requires_dists": [ + "build[virtualenv]>=1.0.3; extra == \"test\"", + "filelock>=3.4.0; extra == \"test\"", + "furo; extra == \"doc\"", + "importlib-metadata>=6; python_version < \"3.10\" and extra == \"core\"", + "importlib-metadata>=7.0.2; python_version < \"3.10\" and extra == \"type\"", + "importlib-resources>=5.10.2; python_version < \"3.9\" and extra == \"core\"", + "ini2toml[lite]>=0.14; extra == \"test\"", + "jaraco.develop>=7.21; (python_version >= \"3.9\" and sys_platform != \"cygwin\") and extra == \"test\"", + "jaraco.develop>=7.21; sys_platform != \"cygwin\" and extra == \"type\"", + "jaraco.envs>=2.2; extra == \"test\"", + "jaraco.packaging>=9.3; extra == \"doc\"", + "jaraco.path>=3.2.0; extra == \"test\"", + "jaraco.test; extra == \"test\"", + "jaraco.text>=3.7; extra == \"core\"", + "jaraco.tidelift>=1.4; extra == \"doc\"", + "more-itertools>=8.8; extra == \"core\"", + "mypy==1.11.*; extra == \"type\"", + "packaging>=23.2; extra == \"test\"", + "packaging>=24; extra == \"core\"", + "pip>=19.1; extra == \"test\"", + "platformdirs>=2.6.2; extra == \"core\"", + "pygments-github-lexers==0.0.5; extra == \"doc\"", + "pyproject-hooks!=1.1; extra == \"doc\"", + "pyproject-hooks!=1.1; extra == \"test\"", + "pytest!=8.1.*,>=6; extra == \"test\"", + "pytest-checkdocs>=2.4; extra == \"check\"", + "pytest-cov; extra == \"cover\"", + "pytest-enabler>=2.2; extra == \"enabler\"", + "pytest-home>=0.5; extra == \"test\"", + "pytest-mypy; extra == \"type\"", + "pytest-perf; sys_platform != \"cygwin\" and extra == \"test\"", + "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", + "pytest-subprocess; extra == \"test\"", + "pytest-timeout; extra == \"test\"", + "pytest-xdist>=3; extra == \"test\"", + "rst.linker>=1.9; extra == \"doc\"", + "ruff>=0.5.2; sys_platform != \"cygwin\" and extra == \"check\"", + "sphinx-favicon; extra == \"doc\"", + "sphinx-inline-tabs; extra == \"doc\"", + "sphinx-lint; extra == \"doc\"", + "sphinx-notfound-page<2,>=1; extra == \"doc\"", + "sphinx-reredirects; extra == \"doc\"", + "sphinx>=3.5; extra == \"doc\"", + "sphinxcontrib-towncrier; extra == \"doc\"", + "tomli-w>=1.0.0; extra == \"test\"", + "tomli>=2.0.1; python_version < \"3.11\" and extra == \"core\"", + "towncrier<24.7; extra == \"doc\"", + "virtualenv>=13.0.0; extra == \"test\"", + "wheel>=0.43.0; extra == \"core\"", + "wheel>=0.44.0; extra == \"test\"" + ], + "requires_python": ">=3.8", + "version": "74.1.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "url": "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz" + } + ], + "project_name": "six", + "requires_dists": [], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", + "version": "1.16.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", + "url": "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz" + } + ], + "project_name": "sniffio", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "1.3.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7", + "url": "https://files.pythonhosted.org/packages/96/7c/a81ef5ef10978dd073a854e0fa93b5d8021d0594b639cc8f6453c3c78a1d/strictyaml-1.7.3-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407", + "url": "https://files.pythonhosted.org/packages/b3/08/efd28d49162ce89c2ad61a88bd80e11fb77bc9f6c145402589112d38f8af/strictyaml-1.7.3.tar.gz" + } + ], + "project_name": "strictyaml", + "requires_dists": [ + "python-dateutil>=2.6.0" + ], + "requires_python": ">=3.7.0", + "version": "1.7.3" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262", + "url": "https://files.pythonhosted.org/packages/3b/98/36187601a15e3d37e9bfcf0e0e1055532b39d044353b06861c3a519737a9/translationstring-1.4-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "bf947538d76e69ba12ab17283b10355a9ecfbc078e6123443f43f2107f6376f3", + "url": "https://files.pythonhosted.org/packages/14/39/32325add93da9439775d7fe4b4887eb7986dbc1d5675b0431f4531f560e5/translationstring-1.4.tar.gz" + } + ], + "project_name": "translationstring", + "requires_dists": [ + "Sphinx>=1.3.1; extra == \"docs\"", + "docutils; extra == \"docs\"", + "pylons-sphinx-themes; extra == \"docs\"" + ], + "requires_python": null, + "version": "1.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "url": "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", + "url": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz" + } + ], + "project_name": "typing-extensions", + "requires_dists": [], + "requires_python": ">=3.8", + "version": "4.12.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "url": "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", + "url": "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz" + } + ], + "project_name": "urllib3", + "requires_dists": [ + "brotli>=1.0.9; platform_python_implementation == \"CPython\" and extra == \"brotli\"", + "brotlicffi>=0.8.0; platform_python_implementation != \"CPython\" and extra == \"brotli\"", + "h2<5,>=4; extra == \"h2\"", + "pysocks!=1.5.7,<2.0,>=1.5.6; extra == \"socks\"", + "zstandard>=0.18.0; extra == \"zstd\"" + ], + "requires_python": ">=3.8", + "version": "2.2.3" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "d1fb1e49927f42573f6c9b7c4fcf61c892af8fdcaa2314daa01d9a560b23488d", + "url": "https://files.pythonhosted.org/packages/2c/d7/36860f68eb977ad685d0f0fda733eca913dbda1bb29bbc5f1c5ba460201a/venusian-3.1.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "eb72cdca6f3139a15dc80f9c95d3c10f8a54a0ba881eeef8e2ec5b42d3ee3a95", + "url": "https://files.pythonhosted.org/packages/f8/39/7c0d9011ec465951aaf71c252effc7c031a04404887422c6f66ba26500e1/venusian-3.1.0.tar.gz" + } + ], + "project_name": "venusian", + "requires_dists": [ + "Sphinx>=4.3.2; extra == \"docs\"", + "coverage; extra == \"testing\"", + "pylons-sphinx-themes; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"", + "repoze.sphinx.autointerface; extra == \"docs\"", + "sphinx-copybutton; extra == \"docs\"" + ], + "requires_python": ">=3.7", + "version": "3.1.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669", + "url": "https://files.pythonhosted.org/packages/5b/a9/485c953a1ac4cb98c28e41fd2c7184072df36bbf99734a51d44d04176878/waitress-3.0.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1", + "url": "https://files.pythonhosted.org/packages/70/34/cb77e5249c433eb177a11ab7425056b32d3b57855377fa1e38b397412859/waitress-3.0.0.tar.gz" + } + ], + "project_name": "waitress", + "requires_dists": [ + "Sphinx>=1.8.1; extra == \"docs\"", + "coverage>=5.0; extra == \"testing\"", + "docutils; extra == \"docs\"", + "pylons-sphinx-themes>=1.0.9; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest; extra == \"testing\"" + ], + "requires_python": ">=3.8.0", + "version": "3.0.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "b60ba63f05c0cf61e086a10c3781a41fcfe30027753a8ae6d819c77592ce83ea", + "url": "https://files.pythonhosted.org/packages/c3/c2/fbc206db211c11ac85f2b440670ff6f43d44d7601f61b95628f56d271c21/WebOb-1.8.8-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "2abc1555e118fc251e705fc6dc66c7f5353bb9fbfab6d20e22f1c02b4b71bcee", + "url": "https://files.pythonhosted.org/packages/a2/7a/ac5b1ab5636cc3bfc9bab1ed54ff4e8fdeb6367edd911f7337be2248b8ab/webob-1.8.8.tar.gz" + } + ], + "project_name": "webob", + "requires_dists": [ + "Sphinx>=1.7.5; extra == \"docs\"", + "coverage; extra == \"testing\"", + "pylons-sphinx-themes; extra == \"docs\"", + "pytest-cov; extra == \"testing\"", + "pytest-xdist; extra == \"testing\"", + "pytest>=3.1.0; extra == \"testing\"" + ], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", + "version": "1.8.8" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "28c2ee983812efb4676d33c7a8c6ade0df191c1c6d652bbbfe6e2eeee067b2d4", + "url": "https://files.pythonhosted.org/packages/c8/7d/24a23d4d6d93744babfb99266eeb97a25ceae58c0f841a872b51c45ee214/zope.deprecation-5.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "b7c32d3392036b2145c40b3103e7322db68662ab09b7267afe1532a9d93f640f", + "url": "https://files.pythonhosted.org/packages/ba/de/a47e434ed1804d82f3fd7561aee5c55914c72d87f54cac6b99c15cbe7f89/zope.deprecation-5.0.tar.gz" + } + ], + "project_name": "zope-deprecation", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "setuptools", + "zope.testrunner; extra == \"test\"" + ], + "requires_python": ">=3.7", + "version": "5.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b", + "url": "https://files.pythonhosted.org/packages/be/56/6a57ef0699b857b33a407162f29eade4062596870d335f53e914bb98fd0e/zope.interface-7.0.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4", + "url": "https://files.pythonhosted.org/packages/14/44/d12683e823ced271ae2ca3976f16066634911e02540a9559b09444a4b2d3/zope.interface-7.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996", + "url": "https://files.pythonhosted.org/packages/2b/6f/059521297028f3037f2b19a711be845983151acbdeda1031749a91d07048/zope.interface-7.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493", + "url": "https://files.pythonhosted.org/packages/2c/c2/39964ef5fed7ac1523bab2d1bba244290965da6f720164b603ec07adf0a7/zope.interface-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca", + "url": "https://files.pythonhosted.org/packages/2d/45/a891ee78ba5ef5b5437394f8c2c56c094ed1ab41a80ef7afe50191dce3d2/zope.interface-7.0.3-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05", + "url": "https://files.pythonhosted.org/packages/3d/ed/0ac414f9373d742d2eb2f436b595ed281031780a405621a4d906096092ea/zope.interface-7.0.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", + "url": "https://files.pythonhosted.org/packages/45/58/890cf943c9a7dd82d096a11872c7efb3f0e97e86f71b886018044fb01972/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", + "url": "https://files.pythonhosted.org/packages/5a/a9/9665ba3aa7c6173ea2c3249c85546139119eaf3146f280cea8053e0047b9/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", + "url": "https://files.pythonhosted.org/packages/6b/c3/7d18af6971634087a4ddc436e37fc47988c31635cd01948ff668d11c96c4/zope.interface-7.0.3-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a", + "url": "https://files.pythonhosted.org/packages/80/ff/66b5cd662b177de4082cac412a877c7a528ef79a392d90e504f50c041dda/zope.interface-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", + "url": "https://files.pythonhosted.org/packages/8a/64/2922134a93978b6a8b823f3e784d6af3d5d165fad1f66388b0f89b5695fc/zope.interface-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32", + "url": "https://files.pythonhosted.org/packages/9e/1b/79bcfbdc7d621c410a188f25d78f6e07aff7f608c9589cfba77003769f98/zope.interface-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3", + "url": "https://files.pythonhosted.org/packages/c1/a3/a890f35a62aa25233c95e2af4510aa1df0553be48450bb0840b8d3b2a62c/zope.interface-7.0.3-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc", + "url": "https://files.pythonhosted.org/packages/c6/91/d3e665df6837629e2eec9cdc8cd1118f1a0e74b586bbec2e6cfc6a1b6c3a/zope.interface-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", + "url": "https://files.pythonhosted.org/packages/c8/83/7de03efae7fc9a4ec64301d86e29a324f32fe395022e3a5b1a79e376668e/zope.interface-7.0.3.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b", + "url": "https://files.pythonhosted.org/packages/ce/bb/51ab7785b2ad3123d5eb85b548f98fe2c0809c6bd452e677b1aca71c3c79/zope.interface-7.0.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd", + "url": "https://files.pythonhosted.org/packages/db/35/c83308ac84552c2242d5d59488dbea9a91c64765e117a71c566ddf896e31/zope.interface-7.0.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", + "url": "https://files.pythonhosted.org/packages/e9/33/a55311169d3d41b61da7c5b7d528ebb0469263252a71d9510849c0d66201/zope.interface-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd", + "url": "https://files.pythonhosted.org/packages/ec/be/6640eb57c4b84a471d691082d0207434d1524e428fba1231c335a4cad446/zope.interface-7.0.3-cp312-cp312-macosx_10_9_x86_64.whl" + } + ], + "project_name": "zope-interface", + "requires_dists": [ + "Sphinx; extra == \"docs\"", + "coverage>=5.0.3; extra == \"test\"", + "coverage>=5.0.3; extra == \"testing\"", + "repoze.sphinx.autointerface; extra == \"docs\"", + "setuptools", + "sphinx-rtd-theme; extra == \"docs\"", + "zope.event; extra == \"test\"", + "zope.event; extra == \"testing\"", + "zope.testing; extra == \"test\"", + "zope.testing; extra == \"testing\"" + ], + "requires_python": ">=3.8", + "version": "7.0.3" + } + ], + "platform_tag": null + } + ], + "only_builds": [], + "only_wheels": [], + "overridden": [], + "path_mappings": {}, + "pex_version": "2.18.1", + "pip_version": "24.2", + "prefer_older_binary": false, + "requirements": [ + "devpi-server==6.12.1" + ], + "requires_python": [ + "<3.14,>=3.10" + ], + "resolver_version": "pip-2020-resolver", + "style": "universal", + "target_systems": [ + "linux", + "mac" + ], + "transitive": true, + "use_pep517": null +} diff --git a/tests/integration/cli/commands/test_venv_create.py b/tests/integration/cli/commands/test_venv_create.py index 3e3e87223..03686c5df 100644 --- a/tests/integration/cli/commands/test_venv_create.py +++ b/tests/integration/cli/commands/test_venv_create.py @@ -3,6 +3,7 @@ from __future__ import absolute_import +import glob import os.path import shutil import subprocess @@ -16,6 +17,7 @@ from pex.cli.commands.venv import InstallLayout from pex.common import open_zip, safe_open from pex.compatibility import commonpath +from pex.dist_metadata import Distribution from pex.interpreter import PythonInterpreter from pex.pep_440 import Version from pex.pep_503 import ProjectName @@ -56,13 +58,35 @@ def cowsay_pex( # type: (...) -> str pex = str(td.join("pex")) - run_pex_command(args=["--lock", lock, "-o", pex]).assert_success() + run_pex_command(args=["--lock", lock, "--include-tools", "-o", pex]).assert_success() assert sorted( [(ProjectName("cowsay"), Version("5.0")), (ProjectName("ansicolors"), Version("1.1.8"))] ) == [(dist.metadata.project_name, dist.metadata.version) for dist in PEX(pex).resolve()] return pex +@pytest.fixture(scope="module") +def pre_resolved_dists( + td, # type: Any + cowsay_pex, # type: str +): + # type: (...) -> str + + dists_dir = str(td.join("dists")) + subprocess.check_call( + args=[cowsay_pex, "repository", "extract", "-f", dists_dir], env=make_env(PEX_TOOLS=1) + ) + assert sorted( + [(ProjectName("cowsay"), Version("5.0")), (ProjectName("ansicolors"), Version("1.1.8"))] + ) == sorted( + [ + (dist.metadata.project_name, dist.metadata.version) + for dist in map(Distribution.load, glob.glob(os.path.join(dists_dir, "*.whl"))) + ] + ) + return dists_dir + + def test_venv_empty(tmpdir): # type: (Any) -> None @@ -110,12 +134,14 @@ def test_venv( tmpdir, # type: Any lock, # type: str cowsay_pex, # type: str + pre_resolved_dists, # type: str ): # type: (...) -> None assert_venv(tmpdir) assert_venv(tmpdir, "--lock", lock) assert_venv(tmpdir, "--pex-repository", cowsay_pex) + assert_venv(tmpdir, "--pre-resolved-dists", pre_resolved_dists) def test_flat_empty(tmpdir): diff --git a/tests/integration/resolve/test_issue_1907.py b/tests/integration/resolve/test_issue_1907.py new file mode 100644 index 000000000..2f569c12d --- /dev/null +++ b/tests/integration/resolve/test_issue_1907.py @@ -0,0 +1,200 @@ +# Copyright 2024 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import + +import glob +import os.path +import re +import subprocess +import sys +from textwrap import dedent + +import pytest + +from pex.atomic_directory import atomic_directory +from pex.common import safe_open +from pex.pep_503 import ProjectName +from pex.pex import PEX +from pex.pip.version import PipVersion +from pex.typing import TYPE_CHECKING +from testing import PY_VER, data, run_pex_command +from testing.cli import run_pex3 + +if TYPE_CHECKING: + from typing import Any + + +skip_unless_supports_devpi_server_lock = pytest.mark.skipif( + PY_VER < (3, 10) or PY_VER >= (3, 14), reason="The uses a lock that requires Python>=3.10,<3.14" +) + + +@pytest.fixture(scope="session") +def dists(shared_integration_test_tmpdir): + # type: (str) -> str + test_issue_1907_chroot = os.path.join(shared_integration_test_tmpdir, "test_issue_1907_chroot") + with atomic_directory(test_issue_1907_chroot) as chroot: + if not chroot.is_finalized(): + requirements = os.path.join(chroot.work_dir, "requirements.txt") + lock = data.path("locks", "devpi-server.lock.json") + run_pex3("lock", "export", "--format", "pip", lock, "-o", requirements).assert_success() + dists = os.path.join(chroot.work_dir, "dists") + subprocess.check_call( + args=[sys.executable, "-m", "pip", "download", "-r", requirements, "-d", dists] + ) + return os.path.join(test_issue_1907_chroot, "dists") + + +@skip_unless_supports_devpi_server_lock +def test_pre_resolved_dists_nominal( + tmpdir, # type: Any + dists, # type: str +): + # type: (...) -> None + + run_pex_command( + args=[ + "--pre-resolved-dists", + dists, + "devpi-server", + "-c", + "devpi-server", + "--", + "--version", + ] + ).assert_success(expected_output_re=re.escape("6.12.1")) + + +@skip_unless_supports_devpi_server_lock +def test_pre_resolved_dists_subset( + tmpdir, # type: Any + dists, # type: str +): + # type: (...) -> None + + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=["--pre-resolved-dists", dists, "pyramid", "-c", "pdistreport", "-o", pex] + ).assert_success() + + assert not any( + dist + for dist in PEX(pex).resolve() + if ProjectName("devpi-server") == dist.metadata.project_name + ), "The subset should not include devpi-server." + + assert subprocess.check_output(args=[pex]).startswith(b"Pyramid version: 2.0.2") + + +@pytest.fixture +def local_project(tmpdir): + # type: (Any) -> str + + project = os.path.join(str(tmpdir), "project") + with safe_open(os.path.join(project, "app.py"), "w") as fp: + fp.write( + dedent( + """\ + import sys + + from pyramid.scripts.pdistreport import main + + + if __name__ == "__main__": + sys.stdout.write("app: ") + main() + """ + ) + ) + with safe_open(os.path.join(project, "pyproject.toml"), "w") as fp: + fp.write( + dedent( + """\ + [build-system] + requires = ["setuptools"] + backend = "setuptools.build_meta" + + [project] + name = "app" + version = "0.1.0" + dependencies = ["pyramid"] + """ + ) + ) + return project + + +@skip_unless_supports_devpi_server_lock +def test_pre_resolved_dists_local_project_requirement( + tmpdir, # type: Any + dists, # type: str + local_project, # type: str +): + # type: (...) -> None + + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=["--pre-resolved-dists", dists, local_project, "-m", "app", "-o", pex] + ).assert_success() + + assert subprocess.check_output(args=[pex]).startswith(b"app: Pyramid version: 2.0.2") + + +@skip_unless_supports_devpi_server_lock +def test_pre_resolved_dists_project_requirement( + tmpdir, # type: Any + dists, # type: str + local_project, # type: str +): + # type: (...) -> None + + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=["--pre-resolved-dists", dists, "--project", local_project, "-m", "app", "-o", pex] + ).assert_success() + + assert subprocess.check_output(args=[pex]).startswith(b"app: Pyramid version: 2.0.2") + + +@skip_unless_supports_devpi_server_lock +def test_pre_resolved_dists_offline( + tmpdir, # type: Any + dists, # type: str + local_project, # type: str +): + # type: (...) -> None + + offline = os.path.join(str(tmpdir), "offline") + os.mkdir(offline) + + # In order to go offline and still be able to build sdists, we need both the un-vendored Pip and + # its basic build requirements. + if PipVersion.DEFAULT is not PipVersion.VENDORED: + args = [sys.executable, "-m", "pip", "wheel", "-w", offline] + args.extend(str(req) for req in PipVersion.DEFAULT.requirements) + subprocess.check_call(args) + + for dist in glob.glob(os.path.join(dists, "*")): + dest_dist = os.path.join(offline, os.path.basename(dist)) + if not os.path.exists(dest_dist): + os.symlink(dist, dest_dist) + + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=[ + "--no-pypi", + "--find-links", + offline, + "--pre-resolved-dists", + offline, + "--project", + local_project, + "-m", + "app", + "-o", + pex, + ] + ).assert_success() + + assert subprocess.check_output(args=[pex]).startswith(b"app: Pyramid version: 2.0.2") diff --git a/tests/integration/test_excludes.py b/tests/integration/test_excludes.py index 057920f9f..80064be19 100644 --- a/tests/integration/test_excludes.py +++ b/tests/integration/test_excludes.py @@ -63,6 +63,7 @@ def requests_certifi_excluded_pex(tmpdir): REQUESTS_LOCK, "--exclude", "certifi", + "--include-tools", "-o", pex, "--pex-root", @@ -154,6 +155,39 @@ def test_exclude( assert_requests_certifi_excluded_pex(pex, certifi_venv) +@skip_unless_compatible_with_requests_lock +def test_pre_resolved_dists_exclude( + tmpdir, # type: Any + certifi_venv, # type: Virtualenv +): + # type: (...) -> None + + pex_repository = requests_certifi_excluded_pex(tmpdir) + dists = os.path.join(str(tmpdir), "dists") + subprocess.check_call( + args=[pex_repository, "repository", "extract", "-f", dists], env=make_env(PEX_TOOLS=1) + ) + + pex_root = PexInfo.from_pex(pex_repository).pex_root + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=[ + "--pre-resolved-dists", + dists, + "--exclude", + "certifi", + "requests", + "-o", + pex, + "--pex-root", + pex_root, + "--runtime-pex-root", + pex_root, + ] + ).assert_success() + assert_requests_certifi_excluded_pex(pex, certifi_venv) + + @skip_unless_compatible_with_requests_lock def test_requirements_pex_exclude( tmpdir, # type: Any diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 5a3367531..3769243e8 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -20,7 +20,7 @@ from pex.cache.dirs import CacheDir from pex.common import is_exe, safe_mkdir, safe_open, safe_rmtree, temporary_dir, touch from pex.compatibility import WINDOWS, commonpath -from pex.dist_metadata import Distribution, Requirement +from pex.dist_metadata import Distribution, Requirement, is_wheel from pex.fetcher import URLFetcher from pex.interpreter import PythonInterpreter from pex.layout import Layout @@ -1360,7 +1360,7 @@ def iter_distributions(pex_root, project_name): for d in dirs: if not d.startswith(project_name): continue - if not d.endswith(".whl"): + if not is_wheel(d): continue wheel_path = os.path.realpath(os.path.join(root, d)) if wheel_path in found: @@ -1843,7 +1843,7 @@ def test_constraint_file_from_url(tmpdir): assert len(dist_paths) == 3 dist_paths.remove("fasteners-0.15-py2.py3-none-any.whl") for dist_path in dist_paths: - assert dist_path.startswith(("six-", "monotonic-")) and dist_path.endswith(".whl") + assert dist_path.startswith(("six-", "monotonic-")) and is_wheel(dist_path) def test_console_script_from_pex_path(tmpdir): diff --git a/tests/integration/test_issue_1179.py b/tests/integration/test_issue_1179.py index 3e68b1f92..66488f71c 100644 --- a/tests/integration/test_issue_1179.py +++ b/tests/integration/test_issue_1179.py @@ -2,9 +2,11 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import sys +from textwrap import dedent import pytest +from pex.targets import LocalInterpreter from testing import run_pex_command @@ -27,10 +29,17 @@ def test_pip_2020_resolver_engaged(): args=["--resolver-version", "pip-legacy-resolver"] + pex_args, quiet=True ) results.assert_failure() - assert "Failed to resolve compatible distributions:" in results.error assert ( - "1: boto3==1.15.6 requires botocore<1.19.0,>=1.18.6 but 1 incompatible dist was resolved: " - "botocore-1.19.63-py2.py3-none-any.whl" in results.error + dedent( + """\ + Failed to resolve compatible distributions for 1 target: + 1: {target} is not compatible with: + boto3 1.15.6 requires botocore<1.19.0,>=1.18.6 but 1 incompatible dist was resolved: + botocore-1.19.63-py2.py3-none-any.whl + """.format( + target=LocalInterpreter.create().render_description() + ) + ) + in results.error ), results.error - run_pex_command(args=["--resolver-version", "pip-2020-resolver"] + pex_args).assert_success() diff --git a/tests/integration/test_overrides.py b/tests/integration/test_overrides.py index fca13a454..c19d5a3eb 100644 --- a/tests/integration/test_overrides.py +++ b/tests/integration/test_overrides.py @@ -6,6 +6,7 @@ import os.path import re import shutil +import subprocess import sys from collections import defaultdict @@ -17,7 +18,7 @@ from pex.pex import PEX from pex.pex_info import PexInfo from pex.typing import TYPE_CHECKING -from testing import PY39, PY310, PY_VER, data, ensure_python_interpreter, run_pex_command +from testing import PY39, PY310, PY_VER, data, ensure_python_interpreter, make_env, run_pex_command from testing.cli import run_pex3 from testing.lock import extract_lock_option_args, index_lock_artifacts @@ -125,6 +126,28 @@ def test_pex_repository_override(tmpdir): ) +@skip_unless_compatible_with_requests_2_31_0 +def test_pre_resolved_dists_override(tmpdir): + # type: (Any) -> None + + repository_pex = os.path.join(str(tmpdir), "repository.pex") + run_pex_command( + args=["requests==2.31.0", "--override", "idna<2.4", "--include-tools", "-o", repository_pex] + ).assert_success() + dists = os.path.join(str(tmpdir), "dists") + subprocess.check_call( + args=[repository_pex, "repository", "extract", "-f", dists], env=make_env(PEX_TOOLS=1) + ) + + pex = os.path.join(str(tmpdir), "pex") + run_pex_command( + args=["--pre-resolved-dists", dists, "requests", "--override", "idna<2.4", "-o", pex] + ).assert_success() + assert_overrides( + pex, expected_overrides=["idna<2.4"], expected_overridden_dists={"idna": ["2.3"]} + ) + + REQUESTS_LOCK = data.path("locks", "requests.lock.json") diff --git a/tests/resolve/test_resolver_options.py b/tests/resolve/test_resolver_options.py index 261fe78b5..f5f686fd3 100644 --- a/tests/resolve/test_resolver_options.py +++ b/tests/resolve/test_resolver_options.py @@ -1,11 +1,13 @@ # Copyright 2021 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). +import os.path import re from argparse import ArgumentParser import pytest +from pex.common import touch from pex.pex_warnings import PEXWarning from pex.pip.version import PipVersion from pex.resolve import resolver_configuration, resolver_options @@ -13,12 +15,13 @@ BuildConfiguration, PexRepositoryConfiguration, PipConfiguration, + PreResolvedConfiguration, ReposConfiguration, ) from pex.typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import List + from typing import Any, List from pex.resolve.resolver_options import ResolverConfiguration @@ -153,6 +156,41 @@ def test_pex_repository(parser): assert "a.pex" == resolver_configuration.pex_repository +def test_pre_resolved_dists( + tmpdir, # type: Any + parser, # type: ArgumentParser +): + # type: (...) -> None + resolver_options.register(parser, include_pre_resolved=True) + + sdist = touch(os.path.join(str(tmpdir), "fake-1.0.tar.gz")) + expected_sdists = [sdist] + + wheel = touch(os.path.join(str(tmpdir), "fake-1.0.py2.py3-none-any.whl")) + expected_wheels = [wheel] + + dists_dir = os.path.join(str(tmpdir), "dists") + touch(os.path.join(dists_dir, "README.md")) + expected_wheels.append(touch(os.path.join(dists_dir, "another-2.0.py3-non-any.whl"))) + expected_sdists.append(touch(os.path.join(dists_dir, "another-2.0.tar.gz"))) + expected_sdists.append(touch(os.path.join(dists_dir, "one_more-3.0.tar.gz"))) + + resolver_configuration = compute_resolver_configuration( + parser, + args=[ + "--pre-resolved-dist", + sdist, + "--pre-resolved-dist", + wheel, + "--pre-resolved-dists", + dists_dir, + ], + ) + assert isinstance(resolver_configuration, PreResolvedConfiguration) + assert sorted(expected_sdists) == sorted(resolver_configuration.sdists) + assert sorted(expected_wheels) == sorted(resolver_configuration.wheels) + + def test_invalid_configuration(parser): # type: (ArgumentParser) -> None resolver_options.register(parser, include_pex_repository=True) diff --git a/tests/test_dist_metadata.py b/tests/test_dist_metadata.py index 4297e8150..bfc1f914c 100644 --- a/tests/test_dist_metadata.py +++ b/tests/test_dist_metadata.py @@ -69,7 +69,7 @@ def downloaded_sdist(requirement): dists = os.listdir(download_dir) assert len(dists) == 1, "Expected 1 dist to be downloaded for {}.".format(requirement) sdist = os.path.join(download_dir, dists[0]) - assert sdist.endswith((".sdist", ".tar.gz", ".zip")) + assert sdist.endswith((".tar.gz", ".zip")) yield sdist diff --git a/tests/test_environment.py b/tests/test_environment.py index cad67934d..5bc776337 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -469,7 +469,7 @@ def test_can_add_handles_optional_build_tag_in_wheel( def test_can_add_handles_invalid_wheel_filename(cpython_38_environment): # type: (PEXEnvironment) -> None dist = create_dist("pep427-invalid.whl") - assert _InvalidWheelName(dist, "pep427-invalid") == cpython_38_environment._can_add(dist) + assert _InvalidWheelName(dist, "pep427-invalid.whl") == cpython_38_environment._can_add(dist) @pytest.fixture diff --git a/tests/test_resolver.py b/tests/test_resolver.py index b068a75b2..8af21d84f 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -12,7 +12,7 @@ import pytest -from pex import targets +from pex import dist_metadata, targets from pex.build_system.pep_517 import build_sdist from pex.common import safe_copy, safe_mkdtemp, temporary_dir from pex.dist_metadata import Distribution, Requirement @@ -523,7 +523,9 @@ def assert_dist( dist = distributions_by_name[project_name] assert version == dist.version - assert is_wheel == (dist.location.endswith(".whl") and zipfile.is_zipfile(dist.location)) + assert is_wheel == ( + dist_metadata.is_wheel(dist.location) and zipfile.is_zipfile(dist.location) + ) assert_dist("project1", "1.0.0", is_wheel=False) assert_dist("project2", "2.0.0", is_wheel=True)