From e663419cf7c2cd97d2159b97213111129a773db6 Mon Sep 17 00:00:00 2001 From: Lequn Chen Date: Wed, 29 Jul 2026 02:14:35 +0000 Subject: [PATCH 1/3] feat(pypi): support non-sha256 --hash pins and index digests pip.parse only recognized --hash=sha256: pins in requirements files and #sha256= URL fragments on Simple API pages; pins using any other algorithm were silently dropped, so they were neither matched against the index metadata nor verified at download time. - Parse --hash=: for all hashlib algorithms and keep the pins in the reconstructed requirement line so that the pip fallback verifies them. - Parse #= Simple API URL fragments per PEP 503. - Match non-sha256 pins against the digests advertised by the index. - Download with ctx.download(integrity = ...) when only a non-sha256 digest is known, via a new integrity attribute on whl_library. - Parse the uv.lock hash field algorithm instead of assuming sha256. - Store ":" values in the lockfile facts for non-sha256 digests, and fix the dist filename fact lookup to use the URL key the filenames are stored under. Matching still requires the index to advertise a digest with the same algorithm as the pin; support for the multi-algorithm PEP 691 hashes dict would be a separate feature. Fixes #3972 Co-Authored-By: Claude Fable 5 --- news/3972.fixed.md | 7 + python/private/pypi/BUILD.bazel | 9 ++ python/private/pypi/hashes.bzl | 142 +++++++++++++++++ python/private/pypi/hub_builder.bzl | 9 +- python/private/pypi/index_sources.bzl | 33 ++-- python/private/pypi/parse_requirements.bzl | 63 ++++++-- python/private/pypi/parse_simpleapi_html.bzl | 31 +++- python/private/pypi/pypi_cache.bzl | 38 +++-- python/private/pypi/whl_library.bzl | 22 ++- tests/pypi/hashes/BUILD.bazel | 3 + tests/pypi/hashes/hashes_tests.bzl | 82 ++++++++++ tests/pypi/hub_builder/hub_builder_tests.bzl | 77 +++++++++ .../index_sources/index_sources_tests.bzl | 82 ++++++++-- .../parse_requirements_tests.bzl | 146 ++++++++++++++++++ .../parse_simpleapi_html_tests.bzl | 65 ++++++++ tests/pypi/pypi_cache/pypi_cache_tests.bzl | 76 +++++++++ 16 files changed, 823 insertions(+), 62 deletions(-) create mode 100644 news/3972.fixed.md create mode 100644 python/private/pypi/hashes.bzl create mode 100644 tests/pypi/hashes/BUILD.bazel create mode 100644 tests/pypi/hashes/hashes_tests.bzl diff --git a/news/3972.fixed.md b/news/3972.fixed.md new file mode 100644 index 0000000000..55fb4ef140 --- /dev/null +++ b/news/3972.fixed.md @@ -0,0 +1,7 @@ +(pypi) Requirement `--hash=:` pins and Simple API +`#=` URL fragments are now parsed for all hash algorithms +instead of silently dropping everything except `sha256`. Non-sha256 pins are +matched against the digests advertised by the index and downloads are verified +using the corresponding Subresource Integrity value, and the pins are kept in +the requirement line when falling back to `pip` +([#3972](https://github.com/bazel-contrib/rules_python/issues/3972)). diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 09ba5ef135..46822773fd 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -166,6 +166,7 @@ bzl_library( srcs = ["hub_builder.bzl"], deps = [ ":attrs", + ":hashes", ":parse_requirements", ":pep508_env", ":pep508_evaluate", @@ -222,6 +223,7 @@ bzl_library( srcs = ["parse_requirements.bzl"], deps = [ ":argparse", + ":hashes", ":index_sources", ":parse_requirements_txt", ":pep508_evaluate", @@ -237,6 +239,7 @@ bzl_library( name = "parse_simpleapi_html", srcs = ["parse_simpleapi_html.bzl"], deps = [ + ":hashes", ":version_from_filename", "//python/private:normalize_name", ], @@ -534,9 +537,15 @@ bzl_library( srcs = ["env_marker_info.bzl"], ) +bzl_library( + name = "hashes", + srcs = ["hashes.bzl"], +) + bzl_library( name = "index_sources", srcs = ["index_sources.bzl"], + deps = [":hashes"], ) bzl_library( diff --git a/python/private/pypi/hashes.bzl b/python/private/pypi/hashes.bzl new file mode 100644 index 0000000000..393b45ff32 --- /dev/null +++ b/python/private/pypi/hashes.bzl @@ -0,0 +1,142 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for working with distribution hash digests. + +Both the requirement `--hash=:` option (PEP 508 tooling) and the +Simple API `#=` URL fragments (PEP 503) or `hashes` dicts +(PEP 691) may use any algorithm from `hashlib.algorithms_guaranteed`, even +though `sha256` is the most common one. +""" + +# The names from `hashlib.algorithms_guaranteed`, which PEP 691 uses as the +# set of valid hash names and PEP 503 strongly recommends fragments to come +# from. +HASH_ALGOS = [ + "blake2b", + "blake2s", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + "sha512", + "shake_128", + "shake_256", +] + +# The algorithms supported by the Subresource Integrity format understood by +# `ctx.download(integrity = ...)`, strongest first. +_SRI_ALGOS = ["sha512", "sha384", "sha256"] + +_HEX = "0123456789abcdef" +_B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +def _hex_to_b64(hex_digest): + if len(hex_digest) % 2: + return None + + data = [] + for i in range(0, len(hex_digest), 2): + hi = _HEX.find(hex_digest[i]) + lo = _HEX.find(hex_digest[i + 1]) + if hi < 0 or lo < 0: + return None + data.append(hi * 16 + lo) + + out = [] + for i in range(0, len(data) // 3 * 3, 3): + n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2] + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append(_B64[(n >> 6) & 63]) + out.append(_B64[n & 63]) + + rem = len(data) % 3 + if rem == 1: + n = data[-1] << 16 + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append("==") + elif rem == 2: + n = (data[-2] << 16) | (data[-1] << 8) + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append(_B64[(n >> 6) & 63]) + out.append("=") + + return "".join(out) + +def hex_to_sri(algo, hex_digest): + """Convert a hex digest to a Subresource Integrity value. + + Args: + algo: {type}`str` the hash algorithm name, e.g. `sha512`. + hex_digest: {type}`str` the hex encoded digest. + + Returns: + {type}`str` the SRI value (e.g. `sha512-...`) that can be passed to + `ctx.download(integrity = ...)` or an empty string if the algorithm + cannot be expressed as SRI or the digest is not valid hex. + """ + if algo not in _SRI_ALGOS or not hex_digest: + return "" + + b64 = _hex_to_b64(hex_digest.lower()) + if b64 == None: + return "" + + return "{}-{}".format(algo, b64) + +def integrity_from_hashes(hashes): + """Get the SRI value of the strongest supported digest. + + Args: + hashes: {type}`dict[str, str]` mapping of hash algorithm name to the + hex encoded digest. + + Returns: + {type}`str` the SRI value or an empty string if none of the digests + use an SRI supported algorithm. + """ + for algo in _SRI_ALGOS: + sri = hex_to_sri(algo, hashes.get(algo, "")) + if sri: + return sri + + return "" + +def preferred_digest(hashes): + """Get a hex digest that identifies an artifact, e.g. for repo naming. + + Args: + hashes: {type}`dict[str, str]` mapping of hash algorithm name to the + hex encoded digest. + + Returns: + {type}`str` the digest of the most preferred algorithm present or an + empty string if there are no digests. + """ + if not hashes: + return "" + + for algo in ["sha256", "sha512", "sha384"]: + if hashes.get(algo): + return hashes[algo] + + return hashes[sorted(hashes.keys())[0]] diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index d7974d39ad..cd548775ec 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -8,6 +8,7 @@ load("//python/private:text_util.bzl", "render") load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") +load(":hashes.bzl", "integrity_from_hashes", "preferred_digest") load(":parse_requirements.bzl", "parse_requirements") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") @@ -677,6 +678,12 @@ def _whl_repo( args["urls"] = [src.url] args["sha256"] = src.sha256 + if not src.sha256: + # The lock file or the index only advertised a non-sha256 digest, use + # the SRI format that the bazel downloader supports for such digests. + integrity = integrity_from_hashes(src.hashes) + if integrity: + args["integrity"] = integrity args["filename"] = src.filename # TODO @aignas 2025-11-02: once we have pipstar enabled we can add extra @@ -685,7 +692,7 @@ def _whl_repo( target_platforms = src.target_platforms if is_multiple_versions else [] return struct( - repo_name = whl_repo_name(src.filename, src.sha256, *target_platforms), + repo_name = whl_repo_name(src.filename, src.sha256 or preferred_digest(src.hashes), *target_platforms), args = args, config_setting = whl_config_setting( version = python_version, diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 1998e4fb33..fa0da0d42e 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -16,6 +16,8 @@ A file that houses private functions used in the `bzlmod` extension with the same name. """ +load(":hashes.bzl", "HASH_ALGOS") + # Just list them here and me super conservative _KNOWN_EXTS = [ # Note, the following source in pip has more extensions @@ -43,8 +45,10 @@ def index_sources(line): line(str): The requirements.txt entry. Returns: - A struct with shas attribute containing: - * `shas` - list[str]; shas to download from pypi_index. + A struct with hashes attribute containing: + * `hashes` - list[str]; `:` hashes of the artifacts + to download from pypi_index. Note that any hash algorithm from + {obj}`HASH_ALGOS` is accepted, not only `sha256`. * `version` - str; version of the package. * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can @@ -58,10 +62,12 @@ def index_sources(line): marker, _, _ = maybe_hashes.partition("--hash=") maybe_hashes = maybe_hashes or line - shas = [ - sha.strip() - for sha in maybe_hashes.split("--hash=sha256:")[1:] - ] + hashes = [] + for h in maybe_hashes.split("--hash=")[1:]: + algo, sep, digest = h.strip().partition(" ")[0].partition(":") + algo = algo.lower() + if sep and digest and algo in HASH_ALGOS: + hashes.append("{}:{}".format(algo, digest)) marker = marker.strip() if head == line: @@ -71,7 +77,7 @@ def index_sources(line): requirement_line = "{} {}".format( requirement, - " ".join(["--hash=sha256:{}".format(sha) for sha in shas]), + " ".join(["--hash={}".format(h) for h in hashes]), ).strip() url = "" @@ -80,9 +86,14 @@ def index_sources(line): maybe_requirement, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() - url, _, sha256 = url.partition("#sha256=") - if sha256: - shas.append(sha256) + url, _, fragment = url.partition("#") + algo, sep, digest = fragment.partition("=") + algo = algo.lower() + if sep and digest and algo in HASH_ALGOS: + hashes.append("{}:{}".format(algo, digest)) + elif fragment: + # Not a hash fragment (e.g. `#egg=`), keep it as part of the URL. + url = "{}#{}".format(url, fragment) _, _, filename = url.rpartition("/") # Replace URL encoded characters and luckily there is only one case @@ -104,7 +115,7 @@ def index_sources(line): requirement = requirement, requirement_line = requirement_line, version = version, - shas = sorted(shas), + hashes = sorted(hashes), marker = marker, url = url, filename = filename, diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 648ec9e65c..110b440792 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -30,6 +30,7 @@ load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_extras_map") # buildifier: disable=bzl-visibility load(":argparse.bzl", "argparse") +load(":hashes.bzl", "preferred_digest") load(":index_sources.bzl", "index_sources") load(":parse_requirements_txt.bzl", "parse_requirements_txt") load(":pep508_evaluate.bzl", "evaluate") @@ -82,7 +83,7 @@ def parse_requirements( * `index_url`: {type}`str` The index URL used to download the package. * `srcs`: {type}`list[struct]` A list of per-distribution source entries, each containing: `distribution`, `extra_pip_args`, `requirement_line`, - `target_platforms`, `filename`, `sha256`, `url`, `yanked`. + `target_platforms`, `filename`, `sha256`, `hashes`, `url`, `yanked`. """ if uv_lock and toml_decode: uv_lock = toml_decode(ctx.read(uv_lock)) @@ -203,11 +204,12 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p for wheel in pkg.get("wheels", []): url = wheel["url"] _, _, filename = url.rpartition("/") - sha256 = wheel.get("hash", "").replace("sha256:", "") + hashes = _parse_uv_lock_hash(wheel.get("hash", "")) candidates.append(struct( filename = filename, url = url, - sha256 = sha256, + sha256 = hashes.get("sha256", ""), + hashes = hashes, kind = "wheel", )) @@ -216,11 +218,12 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p if sdist: url = sdist["url"] _, _, filename = url.rpartition("/") - sha256 = sdist.get("hash", "").replace("sha256:", "") + hashes = _parse_uv_lock_hash(sdist.get("hash", "")) sdist_struct = struct( filename = filename, url = url, - sha256 = sha256, + sha256 = hashes.get("sha256", ""), + hashes = hashes, kind = "sdist", ) @@ -232,6 +235,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p filename = filename, url = url, sha256 = "", + hashes = {}, kind = "git", source = pkg["source"], ) @@ -264,7 +268,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p # Group platforms by resolved source src_to_plats = {} for p, src in plat_to_src.items(): - key = src.filename + src.sha256 + key = src.filename + preferred_digest(src.hashes) src_to_plats.setdefault(key, struct(src = src, plats = [])).plats.append(p) # Build resolved_srcs @@ -283,6 +287,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p target_platforms = plats, filename = src.filename, sha256 = src.sha256, + hashes = src.hashes, url = src.url, yanked = None, )) @@ -309,6 +314,18 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p logger.debug(lambda: "Parsed {} packages from uv.lock".format(len(ret))) return ret +def _parse_uv_lock_hash(hash_str): + """Parse a uv.lock `hash` value of the form `:`. + + The algorithm is not necessarily `sha256` because uv records the strongest + digest that the index offers. + """ + algo, sep, digest = hash_str.partition(":") + if not sep or not digest: + return {} + + return {algo.lower(): digest} + def _parse_requirements_from_req_files( ctx, *, @@ -496,6 +513,7 @@ def _package_srcs( url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ) req_line = r.srcs.requirement_line @@ -516,6 +534,7 @@ def _package_srcs( target_platforms = [], filename = dist.filename, sha256 = dist.sha256, + hashes = dist.hashes, url = dist.url, yanked = dist.yanked, ), @@ -604,10 +623,16 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): return None, True # Handle direct URLs in requirements + url_hashes = {} + for requirement_hash in requirement.srcs.hashes: + algo, _, digest = requirement_hash.partition(":") + url_hashes.setdefault(algo, digest) + dist = struct( url = requirement.srcs.url, filename = requirement.srcs.filename, - sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", + sha256 = url_hashes.get("sha256", ""), + hashes = url_hashes, yanked = None, ) @@ -619,29 +644,35 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): whls = [] sdist = None - # First try to find distributions by SHA256 if provided - shas_to_use = requirement.srcs.shas - if not shas_to_use: + # First try to find distributions by the hashes if provided + hashes_to_use = requirement.srcs.hashes + if not hashes_to_use: version = requirement.srcs.version - shas_to_use = index_urls.sha256s_by_version.get(version, []) - logger.warn(lambda: "requirement file has been generated without hashes, will use all hashes for the given version {} that could find on the index:\n {}".format(version, shas_to_use)) + hashes_to_use = index_urls.sha256s_by_version.get(version, []) + logger.warn(lambda: "requirement file has been generated without hashes, will use all hashes for the given version {} that could find on the index:\n {}".format(version, hashes_to_use)) - for sha256 in shas_to_use: + for requirement_hash in hashes_to_use: # For now if the artifact is marked as yanked we just ignore it. # # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api - maybe_whl = index_urls.whls.get(sha256) + # The hashes from the requirements files are of the `:` form + # whereas the values from the index fallback above are bare digests. + _, sep, digest = requirement_hash.partition(":") + if not sep: + digest = requirement_hash + + maybe_whl = index_urls.whls.get(digest) if maybe_whl and maybe_whl.yanked == None: whls.append(maybe_whl) continue - maybe_sdist = index_urls.sdists.get(sha256) + maybe_sdist = index_urls.sdists.get(digest) if maybe_sdist and maybe_sdist.yanked == None: sdist = maybe_sdist continue - logger.warn(lambda: "Could not find a whl or an sdist with sha256={}".format(sha256)) + logger.warn(lambda: "Could not find a whl or an sdist with hash {}; note that the index may be advertising digests calculated with a different hash algorithm".format(requirement_hash)) yanked = {} for dist in whls + [sdist]: diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl index 7f0d2776d7..19e0ae291e 100644 --- a/python/private/pypi/parse_simpleapi_html.bzl +++ b/python/private/pypi/parse_simpleapi_html.bzl @@ -17,6 +17,7 @@ Parse SimpleAPI HTML in Starlark. """ load("//python/private:normalize_name.bzl", "normalize_name") +load(":hashes.bzl", "HASH_ALGOS") load(":version_from_filename.bzl", "version_from_filename") def parse_simpleapi_html(*, content, parse_index = False): @@ -29,11 +30,17 @@ def parse_simpleapi_html(*, content, parse_index = False): Returns: If it is the index page, return the map of package to URL it can be queried from. - Otherwise, a list of structs with: + Otherwise, a struct with `whls` and `sdists` dicts keyed by the hex digest advertised + in the URL fragment (PEP 503, most commonly `sha256`, but any algorithm from + {obj}`HASH_ALGOS` is accepted) and `sha256s_by_version` mapping each version to the + digests of its artifacts. The dict values are structs with: * filename: {type}`str` The filename of the artifact. * version: {type}`str` The version of the artifact. * url: {type}`str` The URL to download the artifact. - * sha256: {type}`str` The sha256 of the artifact. + * sha256: {type}`str` The sha256 of the artifact, may be empty if the index + advertises a different hash algorithm. + * hashes: {type}`dict[str, str]` The hex digests of the artifact keyed by the hash + algorithm name. * metadata_sha256: {type}`str` The whl METADATA sha256 if we can download it. If this is present, then the 'metadata_url' is also present. Defaults to "". * metadata_url: {type}`str` The URL for the METADATA if we can download it. Defaults to "". @@ -101,7 +108,18 @@ def parse_simpleapi_html(*, content, parse_index = False): continue # 3. Efficient Attribute Parsing - dist_url, _, sha256 = href.partition("#sha256=") + # PEP 503 says the URL fragment SHOULD be `#=` where the + # hash name may be any algorithm from `hashlib`, not only `sha256`. + dist_url, _, fragment = href.partition("#") + algo, sep, digest = fragment.partition("=") + algo = algo.lower() + if not (sep and digest and algo in HASH_ALGOS): + if fragment: + # Not a hash fragment, keep it as part of the URL. + dist_url = "{}#{}".format(dist_url, fragment) + algo = "" + digest = "" + sha256 = digest if algo == "sha256" else "" # Handle Yanked status yanked = None @@ -109,7 +127,7 @@ def parse_simpleapi_html(*, content, parse_index = False): yanked = _unescape_pypi_html(attrs["data-yanked"]) version = version_from_filename(filename) - sha256s_by_version.setdefault(version, []).append(sha256) + sha256s_by_version.setdefault(version, []).append(digest) # 4. Optimized Metadata Check (PEP 714) metadata_sha256 = "" @@ -127,15 +145,16 @@ def parse_simpleapi_html(*, content, parse_index = False): version = version, url = dist_url, sha256 = sha256, + hashes = {algo: digest} if algo else {}, metadata_sha256 = metadata_sha256, metadata_url = metadata_url, yanked = yanked, ) if filename.endswith(".whl"): - whls[sha256] = dist + whls[digest] = dist else: - sdists[sha256] = dist + sdists[digest] = dist if parse_index: return packages diff --git a/python/private/pypi/pypi_cache.bzl b/python/private/pypi/pypi_cache.bzl index d3a3034a79..28784a6a7b 100644 --- a/python/private/pypi/pypi_cache.bzl +++ b/python/private/pypi/pypi_cache.bzl @@ -232,8 +232,14 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver retrieved_versions = {} - for url, sha256 in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): - filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(sha256) + for url, stored_hash in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): + # The values are either bare sha256 hex digests or `:` when the + # index advertises a digest calculated with a different hash algorithm. + algo, sep, digest = stored_hash.partition(":") + if not sep: + algo, digest = "sha256", stored_hash + + filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(url) if not filename: _, _, filename = url.rpartition("/") @@ -251,16 +257,17 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver else: dists = known_sources.setdefault("sdists", {}) - known_sources.setdefault("sha256s_by_version", {}).setdefault(version, []).append(sha256) + known_sources.setdefault("sha256s_by_version", {}).setdefault(version, []).append(digest) - dists.setdefault(sha256, struct( - sha256 = sha256, + dists.setdefault(digest, struct( + sha256 = digest if algo == "sha256" else "", + hashes = {algo: digest} if digest else {}, filename = filename, version = version, metadata_url = "", metadata_sha256 = "", url = url, - yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(sha256), + yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(stored_hash), )) if not known_sources: @@ -318,7 +325,9 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_hashes": { # "": { # "": { - # "": "", + # # A bare sha256 hex digest or ":" when the index + # # advertises a digest calculated with a different hash algorithm. + # "": "", # }, # }, # }, @@ -332,16 +341,23 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_yanked": { # "": { # "": { - # "": "", # if the package is yanked + # "": "", # if the package is yanked # }, # }, # }, # }, - for sha256, d in (value.sdists | value.whls).items(): - facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, sha256) + for digest, d in (value.sdists | value.whls).items(): + stored_hash = digest + if digest and d.sha256 != digest: + for algo, h in d.hashes.items(): + if h == digest: + stored_hash = "{}:{}".format(algo, digest) + break + + facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, stored_hash) if not d.url.endswith(d.filename): facts.setdefault("dist_filenames", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, d.filename) if d.yanked != None: - facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(sha256, d.yanked) + facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(stored_hash, d.yanked) return value diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 9a150db9e8..8e5db5c3df 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -413,10 +413,11 @@ def _whl_archive_impl(rctx): url = urls, output = filename, sha256 = rctx.attr.sha256, + integrity = rctx.attr.integrity if not rctx.attr.sha256 else "", auth = get_auth(rctx, urls), ) - if not rctx.attr.sha256: - # this is only seen when there is a direct URL reference without sha256 + if not rctx.attr.sha256 and not rctx.attr.integrity: + # this is only seen when there is a direct URL reference without a hash logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( rctx.attr.requirement, result.sha256, @@ -454,10 +455,11 @@ def _pip_archive_impl(rctx): url = urls, output = filename, sha256 = rctx.attr.sha256, + integrity = rctx.attr.integrity if not rctx.attr.sha256 else "", auth = get_auth(rctx, urls), ) - if not rctx.attr.sha256: - # this is only seen when there is a direct URL reference without sha256 + if not rctx.attr.sha256 and not rctx.attr.integrity: + # this is only seen when there is a direct URL reference without a hash logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( rctx.attr.requirement, result.sha256, @@ -571,6 +573,17 @@ For example if your whl depends on `numpy` and your Python package repo is named "index_url": attr.string( doc = "The index_url that the package will be downloaded from.", ), + "integrity": attr.string( + doc = """\ +The expected checksum of the downloaded whl in Subresource Integrity format +(e.g. `sha512-...`). Only used when `urls` is passed and `sha256` is empty, +e.g. when the requirements have been locked against an index that advertises +digests using a hash algorithm other than sha256. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), "repo": attr.string( doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", ), @@ -676,6 +689,7 @@ whl_archive = repository_rule( "group_deps", "group_name", "index_url", + "integrity", "repo", "repo_prefix", "requirement", diff --git a/tests/pypi/hashes/BUILD.bazel b/tests/pypi/hashes/BUILD.bazel new file mode 100644 index 0000000000..5c15a2b079 --- /dev/null +++ b/tests/pypi/hashes/BUILD.bazel @@ -0,0 +1,3 @@ +load(":hashes_tests.bzl", "hashes_test_suite") + +hashes_test_suite(name = "hashes_tests") diff --git a/tests/pypi/hashes/hashes_tests.bzl b/tests/pypi/hashes/hashes_tests.bzl new file mode 100644 index 0000000000..2d4c92e737 --- /dev/null +++ b/tests/pypi/hashes/hashes_tests.bzl @@ -0,0 +1,82 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:hashes.bzl", "hex_to_sri", "integrity_from_hashes", "preferred_digest") # buildifier: disable=bzl-visibility + +_tests = [] + +# The digests of b"rules_python" with the SRI values calculated with: +# python3 -c 'import base64, hashlib; h = hashlib.sha512(b"rules_python").digest(); print(base64.b64encode(h))' +_SHA256 = "a90bb21ddf552d508565aa2f0d78ac13297e6c407e39eb578eeac09e62c5da3a" +_SHA256_SRI = "sha256-qQuyHd9VLVCFZaovDXisEyl+bEB+OetXjurAnmLF2jo=" +_SHA384 = "7407ad30f83623cabf9139bd80edb332516880321ab2a20fa0f281767675bfc10b53555eb5042aa58de4b9345f5f70b6" +_SHA384_SRI = "sha384-dAetMPg2I8q/kTm9gO2zMlFogDIasqIPoPKBdnZ1v8ELU1VetQQqpY3kuTRfX3C2" +_SHA512 = "127af31fba52ce5ac96a4bc0f1b31c31c12367feace2b7dbb81066df479883add9dd6166495b126b76ac8c624d254b6a4ba3fbb628e8dcf0a34e1b0b065f619f" +_SHA512_SRI = "sha512-EnrzH7pSzlrJakvA8bMcMcEjZ/6s4rfbuBBm30eYg63Z3WFmSVsSa3asjGJNJUtqS6P7tijo3PCjThsLBl9hnw==" + +def _test_hex_to_sri(env): + env.expect.that_str(hex_to_sri("sha256", _SHA256)).equals(_SHA256_SRI) + env.expect.that_str(hex_to_sri("sha384", _SHA384)).equals(_SHA384_SRI) + env.expect.that_str(hex_to_sri("sha512", _SHA512)).equals(_SHA512_SRI) + + # Upper case digests are accepted. + env.expect.that_str(hex_to_sri("sha512", _SHA512.upper())).equals(_SHA512_SRI) + + # Algorithms that SRI does not support yield nothing. + env.expect.that_str(hex_to_sri("md5", "0" * 32)).equals("") + env.expect.that_str(hex_to_sri("blake2b", "0" * 128)).equals("") + + # Invalid digests yield nothing. + env.expect.that_str(hex_to_sri("sha256", "")).equals("") + env.expect.that_str(hex_to_sri("sha256", "abc")).equals("") + env.expect.that_str(hex_to_sri("sha256", "not-hex!")).equals("") + +_tests.append(_test_hex_to_sri) + +def _test_integrity_from_hashes(env): + env.expect.that_str(integrity_from_hashes({})).equals("") + env.expect.that_str(integrity_from_hashes({"md5": "0" * 32})).equals("") + env.expect.that_str(integrity_from_hashes({"sha512": _SHA512})).equals(_SHA512_SRI) + + # The strongest algorithm wins. + env.expect.that_str(integrity_from_hashes({ + "sha256": _SHA256, + "sha384": _SHA384, + "sha512": _SHA512, + })).equals(_SHA512_SRI) + env.expect.that_str(integrity_from_hashes({ + "sha256": _SHA256, + "sha384": _SHA384, + })).equals(_SHA384_SRI) + +_tests.append(_test_integrity_from_hashes) + +def _test_preferred_digest(env): + env.expect.that_str(preferred_digest({})).equals("") + env.expect.that_str(preferred_digest({"sha256": _SHA256, "sha512": _SHA512})).equals(_SHA256) + env.expect.that_str(preferred_digest({"sha384": _SHA384, "sha512": _SHA512})).equals(_SHA512) + env.expect.that_str(preferred_digest({"blake2b": "deadbeef", "md5": "deadb00f"})).equals("deadbeef") + +_tests.append(_test_preferred_digest) + +def hashes_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index 4651342dd4..0fc51e8e7d 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -358,6 +358,78 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): _tests.append(_test_simple_extras_vs_no_extras_simpleapi) +def _test_simple_sha512_simpleapi(env): + """Non-sha256 pins match the index digests and are downloaded with `integrity`.""" + + def mockread_simpleapi(*_, parse_index, **__): + if parse_index: + content = """\ + simple-0.0.1-py3-none-any.whl
+""" + return struct( + output = parse_simpleapi_html( + content = content, + parse_index = parse_index, + ), + success = True, + ) + + builder = hub_builder( + env, + simpleapi_download_fn = lambda *args, **kwargs: simpleapi_download( + read_simpleapi = mockread_simpleapi, + *args, + **kwargs + ), + ) + builder.pip_parse( + mocks.mctx( + mock_files = { + "win.txt": "simple==0.0.1 --hash=sha512:deadbeef", + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_windows = "win.txt", + experimental_index_url = "https://example.com", + target_platforms = ["windows_aarch64"], + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple_py3_none_any_deadbeef": [ + whl_config_setting( + target_platforms = [ + "cp315_windows_aarch64", + ], + version = "3.15", + ), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple_py3_none_any_deadbeef": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": "https://example.com/simple/", + "integrity": "sha512-3q2+7w==", + "requirement": "simple==0.0.1", + "sha256": "", + "urls": ["/simple-0.0.1-py3-none-any.whl"], + }, + }) + +_tests.append(_test_simple_sha512_simpleapi) + def _test_simple_multiple_python_versions(env): builder = hub_builder( env, @@ -791,6 +863,7 @@ simple==0.0.1 --hash=sha256:deadb00f yanked = None, filename = "simple-0.0.1-py3-none-any.whl", sha256 = "deadb00f", + hashes = {"sha256": "deadb00f"}, url = test.expect_url, ), }, @@ -981,6 +1054,7 @@ def _test_simple_get_index(env): yanked = None, filename = "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", sha256 = "deadb44f", + hashes = {"sha256": "deadb44f"}, url = "example2.org/index/plat_pkg/", ), }, @@ -996,6 +1070,7 @@ def _test_simple_get_index(env): yanked = None, filename = "simple-0.0.1-py3-none-any.whl", sha256 = "deadb00f", + hashes = {"sha256": "deadb00f"}, url = "example2.org", ), }, @@ -1004,6 +1079,7 @@ def _test_simple_get_index(env): yanked = None, filename = "simple-0.0.1.tar.gz", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "example.org", ), }, @@ -1015,6 +1091,7 @@ def _test_simple_get_index(env): yanked = None, filename = "some-other-pkg-0.0.1-py3-none-any.whl", sha256 = "deadb33f", + hashes = {"sha256": "deadb33f"}, url = "example2.org/index/some_other_pkg/", ), }, diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 7aa22d164a..3a605b9eb9 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -26,7 +26,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", marker = "", url = "git+https://github.com/org/foo.git@deadbeef", - shas = [], + hashes = [], version = "", filename = "", ), @@ -59,7 +59,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "0.0.1", filename = "package.whl", ), @@ -68,7 +68,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "0.0.1", filename = "package.whl", ), @@ -80,7 +80,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo[extra] @ https://example.org/foo-1.0.tar.gz --hash=sha256:deadbe0f", marker = "", url = "https://example.org/foo-1.0.tar.gz", - shas = ["deadbe0f"], + hashes = ["sha256:deadbe0f"], version = "", filename = "foo-1.0.tar.gz", ), @@ -89,14 +89,14 @@ def _test_no_simple_api_sources(env): requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=deadbeef", marker = "", url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "", filename = "torch-2.6.0+cpu-cp311-cp311-linux_x86_64.whl", ), } for input, want in inputs.items(): got = index_sources(input) - env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes if hasattr(want, "hashes") else []) env.expect.that_str(got.version).equals(want.version) env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement_line) @@ -109,9 +109,9 @@ _tests.append(_test_no_simple_api_sources) def _test_simple_api_sources(env): tests = { "foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef": struct( - shas = [ - "deadbeef", - "deafbeef", + hashes = [ + "sha256:deadbeef", + "sha256:deafbeef", ], marker = "", requirement = "foo==0.0.2", @@ -119,9 +119,9 @@ def _test_simple_api_sources(env): url = "", ), "foo[extra]==0.0.2; (python_version < 2.7 or extra == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": struct( - shas = [ - "deadbeef", - "deafbeef", + hashes = [ + "sha256:deadbeef", + "sha256:deafbeef", ], marker = "(python_version < 2.7 or extra == \"@\")", requirement = "foo[extra]==0.0.2", @@ -131,7 +131,7 @@ def _test_simple_api_sources(env): } for input, want in tests.items(): got = index_sources(input) - env.expect.that_collection(got.shas).contains_exactly(want.shas) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes) env.expect.that_str(got.version).equals("0.0.2") env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(want.requirement_line) @@ -140,6 +140,62 @@ def _test_simple_api_sources(env): _tests.append(_test_simple_api_sources) +def _test_non_sha256_hashes(env): + tests = { + # A `#sha512=` URL fragment, as any hash algorithm is allowed by PEP 503. + "foo @ https://example.org/foo-0.0.1-py3-none-any.whl#sha512=deadbeef": struct( + hashes = ["sha512:deadbeef"], + marker = "", + requirement = "foo", + requirement_line = "foo @ https://example.org/foo-0.0.1-py3-none-any.whl#sha512=deadbeef", + url = "https://example.org/foo-0.0.1-py3-none-any.whl", + version = "", + filename = "foo-0.0.1-py3-none-any.whl", + ), + # Unknown algorithms are dropped. + "foo==0.0.2 --hash=egg:deadbeef": struct( + hashes = [], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2", + url = "", + version = "0.0.2", + filename = "", + ), + "foo==0.0.2 --hash=sha512:deadbeef": struct( + hashes = ["sha512:deadbeef"], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2 --hash=sha512:deadbeef", + url = "", + version = "0.0.2", + filename = "", + ), + "foo==0.0.2 --hash=sha512:deafbeef --hash=sha256:deadbeef": struct( + hashes = [ + "sha256:deadbeef", + "sha512:deafbeef", + ], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2 --hash=sha512:deafbeef --hash=sha256:deadbeef", + url = "", + version = "0.0.2", + filename = "", + ), + } + for input, want in tests.items(): + got = index_sources(input) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes) + env.expect.that_str(got.version).equals(want.version) + env.expect.that_str(got.requirement).equals(want.requirement) + env.expect.that_str(got.requirement_line).equals(want.requirement_line) + env.expect.that_str(got.marker).equals(want.marker) + env.expect.that_str(got.url).equals(want.url) + env.expect.that_str(got.filename).equals(want.filename) + +_tests.append(_test_non_sha256_hashes) + def index_sources_test_suite(name): """Create the test suite. diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 57b3ea5d3e..76f93999f8 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -92,6 +92,9 @@ foo==0.0.3 --hash=sha256:deadbaaf --hash=sha256:deadb11f --hash=sha256:5d15t --abi=cp39 foo==0.0.3 --hash=sha256:deadbaaf +""", + "requirements_sha512": """\ +foo==0.0.1 --hash=sha512:deadbeef """, "requirements_windows": """\ foo[extra]==0.0.2 --hash=sha256:deadbeef @@ -108,6 +111,7 @@ bar==0.0.1 --hash=sha256:deadb00f "uv_lock_foo_requires_dist_extras": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"root-pkg","source":{"virtual":"."},"version":"0.0.0","dependencies":[{"name":"foo"}],"metadata":{"requires-dist":[{"name":"foo","extras":["all"]}]}}]}""", "uv_lock_foo_resolution_markers_dedup": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","resolution-markers":["sys_platform == 'linux'"],"wheels":[{"hash":"sha256:aaa","url":"https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl"},{"hash":"sha256:bbb","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2","resolution-markers":["sys_platform == 'darwin'"],"wheels":[{"hash":"sha256:ccc","url":"https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl"},{"hash":"sha256:ddd","url":"https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl"}]}]}""", "uv_lock_foo_sdist": """{"package":[{"name":"foo","sdist":{"hash":"sha256:feedcafe","url":"https://files.pythonhosted.org/packages/foo-0.0.1.tar.gz"},"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_sha512": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha512:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", "uv_lock_foo_virtual": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"virtual-pkg","source":{"virtual":true},"version":"0.0.0"}]}""", "uv_lock_foo_with_extras": """{"package":[{"name":"foo","provides-extras":["extra"],"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", "uv_lock_git_vcs": """{"package":[{"name":"foo","source":{"git":"https://github.com/org/foo.git"},"version":"0.1.0"}]}""", @@ -193,6 +197,7 @@ def _test_simple(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -222,6 +227,7 @@ def _test_direct_urls_integration(env): filename = "foo-1.1.tar.gz", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", sha256 = "", + hashes = {}, target_platforms = ["osx_x86_64"], url = "https://github.com/org/foo/downloads/foo-1.1.tar.gz", yanked = None, @@ -232,6 +238,7 @@ def _test_direct_urls_integration(env): filename = "package.whl", requirement_line = "foo[extra]", sha256 = "", + hashes = {}, target_platforms = ["linux_x86_64"], url = "https://some-url/package.whl", yanked = None, @@ -264,6 +271,7 @@ def _test_direct_urls_no_extract(env): filename = "", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", sha256 = "", + hashes = {}, target_platforms = ["osx_x86_64"], url = "", yanked = None, @@ -274,6 +282,7 @@ def _test_direct_urls_no_extract(env): filename = "", requirement_line = "foo[extra] @ https://some-url/package.whl", sha256 = "", + hashes = {}, target_platforms = ["linux_x86_64"], url = "", yanked = None, @@ -309,6 +318,7 @@ def _test_extra_pip_args(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -339,6 +349,7 @@ def _test_dupe_requirements(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -371,6 +382,7 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -389,6 +401,7 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), struct( @@ -399,6 +412,7 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -437,6 +451,7 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -455,6 +470,7 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), struct( @@ -465,6 +481,7 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -523,6 +540,7 @@ def _test_env_marker_resolution(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -541,6 +559,7 @@ def _test_env_marker_resolution(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -572,6 +591,7 @@ def _test_different_package_version(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), struct( @@ -582,6 +602,7 @@ def _test_different_package_version(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -613,6 +634,7 @@ def _test_different_package_extras(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), struct( @@ -623,6 +645,7 @@ def _test_different_package_extras(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -653,6 +676,7 @@ def _test_optional_hash(env): url = "https://example.org/bar-0.0.4.whl", filename = "bar-0.0.4.whl", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -671,6 +695,7 @@ def _test_optional_hash(env): url = "https://example.org/foo-0.0.5.whl", filename = "foo-0.0.5.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, yanked = None, ), ], @@ -701,6 +726,7 @@ def _test_git_sources(env): url = "", filename = "", sha256 = "", + hashes = {}, yanked = None, ), ], @@ -743,6 +769,7 @@ def _test_overlapping_shas_with_index_results(env): "5d15t": struct( url = "sdist", sha256 = "5d15t", + hashes = {"sha256": "5d15t"}, filename = "foo-0.0.1.tar.gz", yanked = None, ), @@ -751,12 +778,14 @@ def _test_overlapping_shas_with_index_results(env): "deadb11f": struct( url = "super2", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", yanked = None, ), "deadbaaf": struct( url = "super2", sha256 = "deadbaaf", + hashes = {"sha256": "deadbaaf"}, filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -778,6 +807,7 @@ def _test_overlapping_shas_with_index_results(env): filename = "foo-0.0.1-py3-none-any.whl", requirement_line = "foo==0.0.3", sha256 = "deadbaaf", + hashes = {"sha256": "deadbaaf"}, target_platforms = ["cp39_linux_x86_64"], url = "super2", yanked = None, @@ -788,6 +818,7 @@ def _test_overlapping_shas_with_index_results(env): filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", requirement_line = "foo==0.0.3", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, target_platforms = ["cp39_osx_x86_64"], url = "super2", yanked = None, @@ -798,6 +829,64 @@ def _test_overlapping_shas_with_index_results(env): _tests.append(_test_overlapping_shas_with_index_results) +def _test_non_sha256_hash_matching(env): + """Test that non-sha256 pins are matched against the index digests.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_sha512": ["cp311_linux_x86_64"], + }, + platforms = { + "cp311_linux_x86_64": struct( + env = pep508_env( + python_version = "3.11.0", + os = "linux", + arch = "x86_64", + ), + whl_abi_tags = ["none"], + whl_platform_tags = ["any"], + ), + }, + get_index_urls = lambda _, __, **kwargs: { + "foo": struct( + index_url = "https://example.com", + sdists = {}, + whls = { + "deadbeef": struct( + url = "https://example.com/foo-0.0.1-py3-none-any.whl", + sha256 = "", + hashes = {"sha512": "deadbeef"}, + filename = "foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + }, + ), + }, + ) + + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://example.com", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + filename = "foo-0.0.1-py3-none-any.whl", + requirement_line = "foo==0.0.1", + sha256 = "", + hashes = {"sha512": "deadbeef"}, + target_platforms = ["cp311_linux_x86_64"], + url = "https://example.com/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_non_sha256_hash_matching) + def _test_get_index_urls_different_versions(env): """Test that different versions from index URLs are matched correctly per platform.""" got = parse_requirements( @@ -835,12 +924,14 @@ def _test_get_index_urls_different_versions(env): "deadb11f": struct( url = "super2", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), "deadbaaf": struct( url = "super2", sha256 = "deadbaaf", + hashes = {"sha256": "deadbaaf"}, filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -862,6 +953,7 @@ def _test_get_index_urls_different_versions(env): filename = "", requirement_line = "boo==0.0.4 --hash=sha256:deadbaaf", sha256 = "", + hashes = {}, target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -880,6 +972,7 @@ def _test_get_index_urls_different_versions(env): filename = "", requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", sha256 = "", + hashes = {}, target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -890,6 +983,7 @@ def _test_get_index_urls_different_versions(env): filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -972,6 +1066,7 @@ def _test_get_index_urls_single_py_version(env): "deadb11f": struct( url = "super2", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), @@ -993,6 +1088,7 @@ def _test_get_index_urls_single_py_version(env): filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -1063,6 +1159,7 @@ def _test_uv_lock_consistent(env): target_platforms = ["linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1091,6 +1188,7 @@ def _test_uv_lock_primary_source(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1100,6 +1198,35 @@ def _test_uv_lock_primary_source(env): _tests.append(_test_uv_lock_primary_source) +def _test_uv_lock_non_sha256_hash(env): + """Test that non-sha256 uv.lock hashes are propagated to the sources.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_sha512", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "", + hashes = {"sha512": "deadbeef"}, + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_non_sha256_hash) + def _test_uv_lock_primary_source_multiple_versions(env): """Test that uv.lock with multiple versions of the same package works.""" got = parse_requirements( @@ -1119,6 +1246,7 @@ def _test_uv_lock_primary_source_multiple_versions(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1129,6 +1257,7 @@ def _test_uv_lock_primary_source_multiple_versions(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.2-py3-none-any.whl", sha256 = "deadb11f", + hashes = {"sha256": "deadb11f"}, url = "https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl", yanked = None, ), @@ -1157,6 +1286,7 @@ def _test_uv_lock_primary_source_with_extras(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1185,6 +1315,7 @@ def _test_uv_lock_primary_source_includes_virtual(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1223,6 +1354,7 @@ def _test_uv_lock_cross_consistent(env): target_platforms = ["linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1251,6 +1383,7 @@ def _test_uv_lock_vcs_entry(env): target_platforms = ["linux_x86_64"], filename = "foo.git", sha256 = "", + hashes = {}, url = "https://github.com/org/foo.git", yanked = None, ), @@ -1279,6 +1412,7 @@ def _test_uv_lock_rules_python_pkg_not_skipped(env): target_platforms = ["linux_x86_64"], filename = "rules_python-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1312,6 +1446,7 @@ def _test_uv_lock_no_consistency_check(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1340,6 +1475,7 @@ def _test_uv_lock_multiple_packages(env): target_platforms = ["linux_x86_64"], filename = "bar-0.0.1.tar.gz", sha256 = "deadb00f", + hashes = {"sha256": "deadb00f"}, url = "https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz", yanked = None, ), @@ -1358,6 +1494,7 @@ def _test_uv_lock_multiple_packages(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1387,6 +1524,7 @@ def _test_uv_lock_with_extra_pip_args(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1419,6 +1557,7 @@ def _test_uv_lock_multi_os_with_requirements(env): target_platforms = ["linux_aarch64", "linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1447,6 +1586,7 @@ def _test_uv_lock_extras_optional_deps(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1475,6 +1615,7 @@ def _test_uv_lock_extras_dep_edge(env): target_platforms = ["linux_x86_64"], filename = "bar-0.0.2-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl", yanked = None, ), @@ -1493,6 +1634,7 @@ def _test_uv_lock_extras_dep_edge(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "baadbeef", + hashes = {"sha256": "baadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1528,6 +1670,7 @@ def _test_uv_lock_wheel_dedup_single_version(env): target_platforms = ["cp39_linux_x86_64"], filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", sha256 = "aaa", + hashes = {"sha256": "aaa"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", yanked = None, ), @@ -1574,6 +1717,7 @@ def _test_uv_lock_wheel_dedup_resolution_markers(env): target_platforms = ["cp39_linux_x86_64"], filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", sha256 = "aaa", + hashes = {"sha256": "aaa"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", yanked = None, ), @@ -1584,6 +1728,7 @@ def _test_uv_lock_wheel_dedup_resolution_markers(env): target_platforms = ["cp39_osx_aarch64"], filename = "foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", sha256 = "ccc", + hashes = {"sha256": "ccc"}, url = "https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", yanked = None, ), @@ -1612,6 +1757,7 @@ def _test_uv_lock_requires_dist_extras(env): target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", sha256 = "deadbeef", + hashes = {"sha256": "deadbeef"}, url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index c84140f459..022184a242 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -296,6 +296,71 @@ def _test_whls(env): _tests.append(_test_whls) +def _test_non_sha256_fragment(env): + """The index may advertise digests calculated with any hash algorithm (PEP 503).""" + + html = _generate_html( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1-py3-none-any.whl#sha512=deadbeef"', + ], + filename = "foo-0.0.1-py3-none-any.whl", + ), + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1.tar.gz#sha512=deadb00f"', + ], + filename = "foo-0.0.1.tar.gz", + ), + struct( + # A non-hash fragment is kept as part of the URL. + attrs = [ + 'href="https://example.org/full-url/foo-0.0.2-py3-none-any.whl#egg=foo"', + ], + filename = "foo-0.0.2-py3-none-any.whl", + ), + ) + got = parse_simpleapi_html(content = html) + + env.expect.that_collection(got.whls).has_size(2) + env.expect.that_collection(got.sdists).has_size(1) + env.expect.that_dict(got.sha256s_by_version).contains_exactly({ + "0.0.1": ["deadbeef", "deadb00f"], + "0.0.2": [""], + }) + + whl = got.whls["deadbeef"] + env.expect.that_str(whl.sha256).equals("") + env.expect.that_dict(whl.hashes).contains_exactly({"sha512": "deadbeef"}) + env.expect.that_str(whl.url).equals("https://example.org/full-url/foo-0.0.1-py3-none-any.whl") + + sdist = got.sdists["deadb00f"] + env.expect.that_str(sdist.sha256).equals("") + env.expect.that_dict(sdist.hashes).contains_exactly({"sha512": "deadb00f"}) + + no_hash_whl = got.whls[""] + env.expect.that_dict(no_hash_whl.hashes).contains_exactly({}) + env.expect.that_str(no_hash_whl.url).equals("https://example.org/full-url/foo-0.0.2-py3-none-any.whl#egg=foo") + +_tests.append(_test_non_sha256_fragment) + +def _test_sha256_fragment_hashes(env): + html = _generate_html( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1-py3-none-any.whl#sha256=deadbeef"', + ], + filename = "foo-0.0.1-py3-none-any.whl", + ), + ) + got = parse_simpleapi_html(content = html) + + whl = got.whls["deadbeef"] + env.expect.that_str(whl.sha256).equals("deadbeef") + env.expect.that_dict(whl.hashes).contains_exactly({"sha256": "deadbeef"}) + +_tests.append(_test_sha256_fragment_hashes) + def parse_simpleapi_html_test_suite(name): """Create the test suite. diff --git a/tests/pypi/pypi_cache/pypi_cache_tests.bzl b/tests/pypi/pypi_cache/pypi_cache_tests.bzl index 14c12ae6d2..fc77413664 100644 --- a/tests/pypi/pypi_cache/pypi_cache_tests.bzl +++ b/tests/pypi/pypi_cache/pypi_cache_tests.bzl @@ -98,6 +98,8 @@ def _test_pypi_cache_writes_to_facts(env): version = "1.0.0", filename = "pkg-1.0.0.tar.gz", url = "https://pypi.org/files/pkg-1.0.0.tar.gz", + sha256 = "sha_sdist", + hashes = {"sha256": "sha_sdist"}, yanked = "", ), }, @@ -106,6 +108,8 @@ def _test_pypi_cache_writes_to_facts(env): version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + sha256 = "sha_whl", + hashes = {"sha256": "sha_whl"}, yanked = "Security issue", ), # This won't get stored @@ -113,6 +117,8 @@ def _test_pypi_cache_writes_to_facts(env): version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl", + sha256 = "sha_whl_2", + hashes = {"sha256": "sha_whl_2"}, yanked = None, ), }, @@ -231,6 +237,7 @@ def _test_pypi_cache_reads_from_facts(env): sdists = { "sha_sdist": struct( sha256 = "sha_sdist", + hashes = {"sha256": "sha_sdist"}, version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -242,6 +249,7 @@ def _test_pypi_cache_reads_from_facts(env): whls = { "sha_whl": struct( sha256 = "sha_whl", + hashes = {"sha256": "sha_whl"}, version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", @@ -294,6 +302,7 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): sdists = { "sha_sdist_1.0.0": struct( sha256 = "sha_sdist_1.0.0", + hashes = {"sha256": "sha_sdist_1.0.0"}, version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -305,6 +314,7 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): whls = { "sha_whl_1.0.0": struct( sha256 = "sha_whl_1.0.0", + hashes = {"sha256": "sha_whl_1.0.0"}, version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", metadata_url = "", @@ -336,6 +346,72 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): _tests.append(_test_pypi_cache_reads_from_facts_drops_unaccessed_dists) +def _test_pypi_cache_facts_non_sha256_round_trip(env): + """Verifies that digests of other hash algorithms survive the facts round trip.""" + mock_ctx = mocks.mctx(facts = {}) + cache = _cache(env, mctx = mock_ctx) + + fake_result = struct( + sdists = {}, + whls = { + "whl_digest": struct( + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + sha256 = "", + hashes = {"sha512": "whl_digest"}, + yanked = None, + ), + }, + sha256s_by_version = { + "1.0.0": ["whl_digest"], + }, + ) + + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0"]) + cache.setdefault(key, fake_result) + + # The hash algorithm is stored in the facts + cache.get_facts().contains_exactly({ + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha512:whl_digest", + }, + }, + }, + "fact_version": "v1", # Facts version + }) + + # And a cache that only has the facts reconstructs the digests + cache = _cache(env, mctx = mocks.mctx(facts = { + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha512:whl_digest", + }, + }, + }, + "fact_version": "v1", # Facts version + })) + + got = cache.get(key) + got.whls().contains_exactly({ + "whl_digest": struct( + sha256 = "", + hashes = {"sha512": "whl_digest"}, + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + metadata_url = "", + metadata_sha256 = "", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + yanked = None, + ), + }) + got.sha256s_by_version().contains_exactly({"1.0.0": ["whl_digest"]}) + +_tests.append(_test_pypi_cache_facts_non_sha256_round_trip) + def _test_memory_cache_index_urls(env): """Verifies that the cache returns stored values for index_urls.""" store = {} From 97f83d77ab48b451661bb7dc05658d54814ff698 Mon Sep 17 00:00:00 2001 From: Lequn Chen Date: Thu, 30 Jul 2026 07:09:40 +0000 Subject: [PATCH 2/3] refactor(pypi): normalize digest handling around canonical : strings Address review feedback from @aignas on #3974: - rename hashes.bzl to hash.bzl, exporting a single `hash` struct (ALGOS, digest, hex_to_sri, integrity, preferred_digest) - carry one canonical `:` string per artifact instead of parallel `sha256` + `hashes` fields; requirement pins match the index via a direct dict lookup - rename `sha256s_by_version` to `hashes_by_version` everywhere - always pass the digest to whl_library as an SRI `integrity` value instead of `sha256`; fail loudly when a sha256/384/512 digest is not valid hex rather than silently disabling verification - store facts as `:` unconditionally and bump _FACT_VERSION to v2, dropping the legacy bare-sha256 read branch Repo names are unchanged for sha256 digests: whl_repo_name strips the algo prefix and sha256 stays the preferred algorithm for naming. Co-Authored-By: Claude Fable 5 --- docs/pypi/download.md | 5 +- news/3972.fixed.md | 4 + python/private/pypi/BUILD.bazel | 12 +- python/private/pypi/extension.bzl | 4 +- python/private/pypi/{hashes.bzl => hash.bzl} | 95 ++++++--- python/private/pypi/hub_builder.bzl | 17 +- python/private/pypi/index_sources.bzl | 20 +- python/private/pypi/parse_requirements.bzl | 58 ++---- python/private/pypi/parse_simpleapi_html.bzl | 41 ++-- python/private/pypi/pypi_cache.bzl | 62 +++--- python/private/pypi/simpleapi_download.bzl | 4 +- python/private/pypi/whl_library.bzl | 5 +- python/private/pypi/whl_repo_name.bzl | 16 +- tests/pypi/hash/BUILD.bazel | 3 + tests/pypi/hash/hash_tests.bzl | 102 ++++++++++ tests/pypi/hashes/BUILD.bazel | 3 - tests/pypi/hashes/hashes_tests.bzl | 82 -------- tests/pypi/hub_builder/hub_builder_tests.bzl | 64 +++--- .../parse_requirements_tests.bzl | 188 ++++++------------ .../parse_simpleapi_html_tests.bzl | 57 +++--- tests/pypi/pypi_cache/pypi_cache_tests.bzl | 161 +++++++-------- .../simpleapi_download_tests.bzl | 16 +- .../whl_repo_name/whl_repo_name_tests.bzl | 12 ++ 23 files changed, 501 insertions(+), 530 deletions(-) rename python/private/pypi/{hashes.bzl => hash.bzl} (54%) create mode 100644 tests/pypi/hash/BUILD.bazel create mode 100644 tests/pypi/hash/hash_tests.bzl delete mode 100644 tests/pypi/hashes/BUILD.bazel delete mode 100644 tests/pypi/hashes/hashes_tests.bzl diff --git a/docs/pypi/download.md b/docs/pypi/download.md index e819ed0791..fb7f38dc24 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -342,8 +342,9 @@ This does not mean that `rules_python` is fetching the wheels eagerly; rather, it means that it is calling the PyPI server to get the Simple API response to get the list of all available source and wheel distributions. Once it has gotten all of the available distributions, it will select the right ones depending -on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes -are not present in the requirements file, we will fall back to matching by version +on the `--hash` values in your `requirements_lock.txt` file (any hash algorithm +advertised by the index can be matched, not only `sha256`). If hashes are not +present in the requirements file, we will fall back to matching by version specified in the lock file. Fetching the distribution information from the PyPI allows `rules_python` to diff --git a/news/3972.fixed.md b/news/3972.fixed.md index 55fb4ef140..f96f803988 100644 --- a/news/3972.fixed.md +++ b/news/3972.fixed.md @@ -5,3 +5,7 @@ matched against the digests advertised by the index and downloads are verified using the corresponding Subresource Integrity value, and the pins are kept in the requirement line when falling back to `pip` ([#3972](https://github.com/bazel-contrib/rules_python/issues/3972)). +As part of this, `whl_library` repos created by `pip.parse` now always pass +the digest via the `integrity` attribute (SRI format) instead of `sha256`, +and the lock file facts store digests as `:` values (the facts +version was bumped, so cached index information is refreshed once). diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 46822773fd..775aa0d9d2 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -166,7 +166,7 @@ bzl_library( srcs = ["hub_builder.bzl"], deps = [ ":attrs", - ":hashes", + ":hash", ":parse_requirements", ":pep508_env", ":pep508_evaluate", @@ -223,7 +223,7 @@ bzl_library( srcs = ["parse_requirements.bzl"], deps = [ ":argparse", - ":hashes", + ":hash", ":index_sources", ":parse_requirements_txt", ":pep508_evaluate", @@ -239,7 +239,7 @@ bzl_library( name = "parse_simpleapi_html", srcs = ["parse_simpleapi_html.bzl"], deps = [ - ":hashes", + ":hash", ":version_from_filename", "//python/private:normalize_name", ], @@ -538,14 +538,14 @@ bzl_library( ) bzl_library( - name = "hashes", - srcs = ["hashes.bzl"], + name = "hash", + srcs = ["hash.bzl"], ) bzl_library( name = "index_sources", srcs = ["index_sources.bzl"], - deps = [":hashes"], + deps = [":hash"], ) bzl_library( diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 412b03c21f..210e169d4d 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -692,8 +692,8 @@ This value is going to be subject to `envsubst` substitutions if necessary, look The indexes must support Simple API as described here: https://packaging.python.org/en/latest/specifications/simple-repository-api/ -Index metadata will be used to get `sha256` values for packages even if the -`sha256` values are not present in the requirements.txt lock file. +Index metadata will be used to get the hash digest values for packages even +if the `--hash` values are not present in the requirements.txt lock file. Defaults to `https://pypi.org/simple`. diff --git a/python/private/pypi/hashes.bzl b/python/private/pypi/hash.bzl similarity index 54% rename from python/private/pypi/hashes.bzl rename to python/private/pypi/hash.bzl index 393b45ff32..bcdfaecf00 100644 --- a/python/private/pypi/hashes.bzl +++ b/python/private/pypi/hash.bzl @@ -18,12 +18,17 @@ Both the requirement `--hash=:` option (PEP 508 tooling) and the Simple API `#=` URL fragments (PEP 503) or `hashes` dicts (PEP 691) may use any algorithm from `hashlib.algorithms_guaranteed`, even though `sha256` is the most common one. + +Internally a single digest is always represented as the canonical +`:` string (the same shape as the pip `--hash` values and +the uv.lock `hash` values) and only converted to the Subresource Integrity +format at the `ctx.download(integrity = ...)` boundary. """ # The names from `hashlib.algorithms_guaranteed`, which PEP 691 uses as the # set of valid hash names and PEP 503 strongly recommends fragments to come # from. -HASH_ALGOS = [ +_ALGOS = [ "blake2b", "blake2s", "md5", @@ -44,6 +49,11 @@ HASH_ALGOS = [ # `ctx.download(integrity = ...)`, strongest first. _SRI_ALGOS = ["sha512", "sha384", "sha256"] +# The order used to pick the digest identifying an artifact when several are +# available: `sha256` first so that repo names stay stable for the common +# case, then the remaining algorithms that the bazel downloader can verify. +_PREFERRED_ALGOS = ["sha256", "sha512", "sha384"] + _HEX = "0123456789abcdef" _B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -82,7 +92,24 @@ def _hex_to_b64(hex_digest): return "".join(out) -def hex_to_sri(algo, hex_digest): +def _digest(algo, hex_digest): + """Construct a canonical `:` digest string. + + Args: + algo: {type}`str` the hash algorithm name, e.g. `sha256`. + hex_digest: {type}`str` the hex encoded digest. + + Returns: + {type}`str` the canonical digest string or an empty string if the + algorithm is not a known `hashlib` algorithm or the digest is empty. + """ + algo = algo.lower() + if not hex_digest or algo not in _ALGOS: + return "" + + return "{}:{}".format(algo, hex_digest) + +def _hex_to_sri(algo, hex_digest): """Convert a hex digest to a Subresource Integrity value. Args: @@ -103,40 +130,60 @@ def hex_to_sri(algo, hex_digest): return "{}-{}".format(algo, b64) -def integrity_from_hashes(hashes): - """Get the SRI value of the strongest supported digest. +def _integrity(digest): + """Get the SRI value for a canonical `:` string. Args: - hashes: {type}`dict[str, str]` mapping of hash algorithm name to the - hex encoded digest. + digest: {type}`str` the canonical digest string. Returns: - {type}`str` the SRI value or an empty string if none of the digests - use an SRI supported algorithm. + {type}`str` the SRI value (e.g. `sha256-...`) that can be passed to + `ctx.download(integrity = ...)` or an empty string if the algorithm + cannot be expressed as SRI. Fails if the algorithm is SRI supported + but the digest is not valid hex, because silently returning an empty + string would disable the download verification for an artifact that + should be verifiable. """ - for algo in _SRI_ALGOS: - sri = hex_to_sri(algo, hashes.get(algo, "")) - if sri: - return sri + algo, _, hex_digest = digest.partition(":") + if algo not in _SRI_ALGOS or not hex_digest: + return "" - return "" + sri = _hex_to_sri(algo, hex_digest) + if not sri: + fail("Invalid {} digest: {}".format(algo, hex_digest)) -def preferred_digest(hashes): - """Get a hex digest that identifies an artifact, e.g. for repo naming. + return sri + +def _preferred_digest(digests): + """Pick the digest that identifies an artifact, e.g. for repo naming. Args: - hashes: {type}`dict[str, str]` mapping of hash algorithm name to the - hex encoded digest. + digests: {type}`list[str]` canonical `:` strings. Returns: - {type}`str` the digest of the most preferred algorithm present or an - empty string if there are no digests. + {type}`str` the canonical digest string of the most preferred + algorithm present or an empty string if there are no digests. """ - if not hashes: + by_algo = {} + for digest in digests: + if not digest: + continue + algo, _, _ = digest.partition(":") + by_algo.setdefault(algo, digest) + + if not by_algo: return "" - for algo in ["sha256", "sha512", "sha384"]: - if hashes.get(algo): - return hashes[algo] + for algo in _PREFERRED_ALGOS: + if algo in by_algo: + return by_algo[algo] + + return by_algo[sorted(by_algo.keys())[0]] - return hashes[sorted(hashes.keys())[0]] +hash = struct( + ALGOS = _ALGOS, + digest = _digest, + hex_to_sri = _hex_to_sri, + integrity = _integrity, + preferred_digest = _preferred_digest, +) diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index cd548775ec..596cfb2cbe 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -8,7 +8,7 @@ load("//python/private:text_util.bzl", "render") load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") -load(":hashes.bzl", "integrity_from_hashes", "preferred_digest") +load(":hash.bzl", "hash") load(":parse_requirements.bzl", "parse_requirements") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") @@ -677,13 +677,12 @@ def _whl_repo( args["index_url"] = index_url args["urls"] = [src.url] - args["sha256"] = src.sha256 - if not src.sha256: - # The lock file or the index only advertised a non-sha256 digest, use - # the SRI format that the bazel downloader supports for such digests. - integrity = integrity_from_hashes(src.hashes) - if integrity: - args["integrity"] = integrity + + # The digest is always passed in the SRI format that the bazel downloader + # supports. It may be empty if the digest uses a hash algorithm that + # cannot be expressed as SRI (e.g. `md5`), in which case `whl_library` + # will download without verification and warn. + args["integrity"] = hash.integrity(src.digest) args["filename"] = src.filename # TODO @aignas 2025-11-02: once we have pipstar enabled we can add extra @@ -692,7 +691,7 @@ def _whl_repo( target_platforms = src.target_platforms if is_multiple_versions else [] return struct( - repo_name = whl_repo_name(src.filename, src.sha256 or preferred_digest(src.hashes), *target_platforms), + repo_name = whl_repo_name(src.filename, src.digest, *target_platforms), args = args, config_setting = whl_config_setting( version = python_version, diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index fa0da0d42e..4353f5051a 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -16,7 +16,7 @@ A file that houses private functions used in the `bzlmod` extension with the same name. """ -load(":hashes.bzl", "HASH_ALGOS") +load(":hash.bzl", "hash") # Just list them here and me super conservative _KNOWN_EXTS = [ @@ -48,7 +48,7 @@ def index_sources(line): A struct with hashes attribute containing: * `hashes` - list[str]; `:` hashes of the artifacts to download from pypi_index. Note that any hash algorithm from - {obj}`HASH_ALGOS` is accepted, not only `sha256`. + {obj}`hash.ALGOS` is accepted, not only `sha256`. * `version` - str; version of the package. * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can @@ -64,10 +64,10 @@ def index_sources(line): maybe_hashes = maybe_hashes or line hashes = [] for h in maybe_hashes.split("--hash=")[1:]: - algo, sep, digest = h.strip().partition(" ")[0].partition(":") - algo = algo.lower() - if sep and digest and algo in HASH_ALGOS: - hashes.append("{}:{}".format(algo, digest)) + algo, _, hex_digest = h.strip().partition(" ")[0].partition(":") + digest = hash.digest(algo, hex_digest) + if digest: + hashes.append(digest) marker = marker.strip() if head == line: @@ -87,10 +87,10 @@ def index_sources(line): url = url_and_rest.strip().partition(" ")[0].strip() url, _, fragment = url.partition("#") - algo, sep, digest = fragment.partition("=") - algo = algo.lower() - if sep and digest and algo in HASH_ALGOS: - hashes.append("{}:{}".format(algo, digest)) + algo, _, hex_digest = fragment.partition("=") + digest = hash.digest(algo, hex_digest) + if digest: + hashes.append(digest) elif fragment: # Not a hash fragment (e.g. `#egg=`), keep it as part of the URL. url = "{}#{}".format(url, fragment) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 110b440792..4874c7ac79 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -30,7 +30,7 @@ load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_extras_map") # buildifier: disable=bzl-visibility load(":argparse.bzl", "argparse") -load(":hashes.bzl", "preferred_digest") +load(":hash.bzl", "hash") load(":index_sources.bzl", "index_sources") load(":parse_requirements_txt.bzl", "parse_requirements_txt") load(":pep508_evaluate.bzl", "evaluate") @@ -83,7 +83,8 @@ def parse_requirements( * `index_url`: {type}`str` The index URL used to download the package. * `srcs`: {type}`list[struct]` A list of per-distribution source entries, each containing: `distribution`, `extra_pip_args`, `requirement_line`, - `target_platforms`, `filename`, `sha256`, `hashes`, `url`, `yanked`. + `target_platforms`, `filename`, `digest`, `url`, `yanked`. The `digest` + is a `:` string or empty if the artifact digest is unknown. """ if uv_lock and toml_decode: uv_lock = toml_decode(ctx.read(uv_lock)) @@ -204,12 +205,10 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p for wheel in pkg.get("wheels", []): url = wheel["url"] _, _, filename = url.rpartition("/") - hashes = _parse_uv_lock_hash(wheel.get("hash", "")) candidates.append(struct( filename = filename, url = url, - sha256 = hashes.get("sha256", ""), - hashes = hashes, + digest = _parse_uv_lock_hash(wheel.get("hash", "")), kind = "wheel", )) @@ -218,12 +217,10 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p if sdist: url = sdist["url"] _, _, filename = url.rpartition("/") - hashes = _parse_uv_lock_hash(sdist.get("hash", "")) sdist_struct = struct( filename = filename, url = url, - sha256 = hashes.get("sha256", ""), - hashes = hashes, + digest = _parse_uv_lock_hash(sdist.get("hash", "")), kind = "sdist", ) @@ -234,8 +231,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p git_struct = struct( filename = filename, url = url, - sha256 = "", - hashes = {}, + digest = "", kind = "git", source = pkg["source"], ) @@ -268,7 +264,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p # Group platforms by resolved source src_to_plats = {} for p, src in plat_to_src.items(): - key = src.filename + preferred_digest(src.hashes) + key = src.filename + src.digest src_to_plats.setdefault(key, struct(src = src, plats = [])).plats.append(p) # Build resolved_srcs @@ -286,8 +282,7 @@ def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, p requirement_line = requirement_line, target_platforms = plats, filename = src.filename, - sha256 = src.sha256, - hashes = src.hashes, + digest = src.digest, url = src.url, yanked = None, )) @@ -320,11 +315,8 @@ def _parse_uv_lock_hash(hash_str): The algorithm is not necessarily `sha256` because uv records the strongest digest that the index offers. """ - algo, sep, digest = hash_str.partition(":") - if not sep or not digest: - return {} - - return {algo.lower(): digest} + algo, _, hex_digest = hash_str.partition(":") + return hash.digest(algo, hex_digest) def _parse_requirements_from_req_files( ctx, @@ -512,8 +504,7 @@ def _package_srcs( dist = struct( url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ) req_line = r.srcs.requirement_line @@ -533,8 +524,7 @@ def _package_srcs( requirement_line = req_line, target_platforms = [], filename = dist.filename, - sha256 = dist.sha256, - hashes = dist.hashes, + digest = dist.digest, url = dist.url, yanked = dist.yanked, ), @@ -623,16 +613,10 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): return None, True # Handle direct URLs in requirements - url_hashes = {} - for requirement_hash in requirement.srcs.hashes: - algo, _, digest = requirement_hash.partition(":") - url_hashes.setdefault(algo, digest) - dist = struct( url = requirement.srcs.url, filename = requirement.srcs.filename, - sha256 = url_hashes.get("sha256", ""), - hashes = url_hashes, + digest = hash.preferred_digest(requirement.srcs.hashes), yanked = None, ) @@ -648,20 +632,14 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): hashes_to_use = requirement.srcs.hashes if not hashes_to_use: version = requirement.srcs.version - hashes_to_use = index_urls.sha256s_by_version.get(version, []) + hashes_to_use = index_urls.hashes_by_version.get(version, []) logger.warn(lambda: "requirement file has been generated without hashes, will use all hashes for the given version {} that could find on the index:\n {}".format(version, hashes_to_use)) - for requirement_hash in hashes_to_use: + for digest in hashes_to_use: # For now if the artifact is marked as yanked we just ignore it. # # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api - # The hashes from the requirements files are of the `:` form - # whereas the values from the index fallback above are bare digests. - _, sep, digest = requirement_hash.partition(":") - if not sep: - digest = requirement_hash - maybe_whl = index_urls.whls.get(digest) if maybe_whl and maybe_whl.yanked == None: whls.append(maybe_whl) @@ -672,7 +650,7 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): sdist = maybe_sdist continue - logger.warn(lambda: "Could not find a whl or an sdist with hash {}; note that the index may be advertising digests calculated with a different hash algorithm".format(requirement_hash)) + logger.warn(lambda: "Could not find a whl or an sdist with hash {}; note that the index may be advertising digests calculated with a different hash algorithm".format(digest)) yanked = {} for dist in whls + [sdist]: @@ -693,8 +671,8 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): if not whls and not sdist: # If there are no suitable wheels to handle for now allow fallback to pip, it # may be a little bit more helpful when debugging? Most likely something is - # going a bit wrong here, should we raise an error because the sha256 have most - # likely mismatched? We are already printing a warning above. + # going a bit wrong here, should we raise an error because the digests have + # most likely mismatched? We are already printing a warning above. return None, True # Select a single wheel that can work on the target_platform diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl index 19e0ae291e..e919c8ca55 100644 --- a/python/private/pypi/parse_simpleapi_html.bzl +++ b/python/private/pypi/parse_simpleapi_html.bzl @@ -17,11 +17,11 @@ Parse SimpleAPI HTML in Starlark. """ load("//python/private:normalize_name.bzl", "normalize_name") -load(":hashes.bzl", "HASH_ALGOS") +load(":hash.bzl", "hash") load(":version_from_filename.bzl", "version_from_filename") def parse_simpleapi_html(*, content, parse_index = False): - """Get the package URLs for given shas by parsing the Simple API HTML. + """Get the package URLs for given digests by parsing the Simple API HTML. Args: content: {type}`str` The Simple API HTML content. @@ -30,17 +30,15 @@ def parse_simpleapi_html(*, content, parse_index = False): Returns: If it is the index page, return the map of package to URL it can be queried from. - Otherwise, a struct with `whls` and `sdists` dicts keyed by the hex digest advertised - in the URL fragment (PEP 503, most commonly `sha256`, but any algorithm from - {obj}`HASH_ALGOS` is accepted) and `sha256s_by_version` mapping each version to the - digests of its artifacts. The dict values are structs with: + Otherwise, a struct with `whls` and `sdists` dicts keyed by the `:` + value advertised in the URL fragment (PEP 503, most commonly `sha256`, but any + algorithm from {obj}`hash.ALGOS` is accepted) and `hashes_by_version` mapping each + version to the digests of its artifacts. The dict values are structs with: * filename: {type}`str` The filename of the artifact. * version: {type}`str` The version of the artifact. * url: {type}`str` The URL to download the artifact. - * sha256: {type}`str` The sha256 of the artifact, may be empty if the index - advertises a different hash algorithm. - * hashes: {type}`dict[str, str]` The hex digests of the artifact keyed by the hash - algorithm name. + * digest: {type}`str` The `:` value of the artifact, may be empty + if the index does not advertise any digest. * metadata_sha256: {type}`str` The whl METADATA sha256 if we can download it. If this is present, then the 'metadata_url' is also present. Defaults to "". * metadata_url: {type}`str` The URL for the METADATA if we can download it. Defaults to "". @@ -50,7 +48,7 @@ def parse_simpleapi_html(*, content, parse_index = False): """ sdists = {} whls = {} - sha256s_by_version = {} + hashes_by_version = {} # 1. Faster Version Extraction # Search only the first 2KB for versioning metadata instead of splitting everything @@ -111,15 +109,11 @@ def parse_simpleapi_html(*, content, parse_index = False): # PEP 503 says the URL fragment SHOULD be `#=` where the # hash name may be any algorithm from `hashlib`, not only `sha256`. dist_url, _, fragment = href.partition("#") - algo, sep, digest = fragment.partition("=") - algo = algo.lower() - if not (sep and digest and algo in HASH_ALGOS): - if fragment: - # Not a hash fragment, keep it as part of the URL. - dist_url = "{}#{}".format(dist_url, fragment) - algo = "" - digest = "" - sha256 = digest if algo == "sha256" else "" + algo, _, hex_digest = fragment.partition("=") + digest = hash.digest(algo, hex_digest) + if not digest and fragment: + # Not a hash fragment, keep it as part of the URL. + dist_url = "{}#{}".format(dist_url, fragment) # Handle Yanked status yanked = None @@ -127,7 +121,7 @@ def parse_simpleapi_html(*, content, parse_index = False): yanked = _unescape_pypi_html(attrs["data-yanked"]) version = version_from_filename(filename) - sha256s_by_version.setdefault(version, []).append(digest) + hashes_by_version.setdefault(version, []).append(digest) # 4. Optimized Metadata Check (PEP 714) metadata_sha256 = "" @@ -144,8 +138,7 @@ def parse_simpleapi_html(*, content, parse_index = False): filename = filename, version = version, url = dist_url, - sha256 = sha256, - hashes = {algo: digest} if algo else {}, + digest = digest, metadata_sha256 = metadata_sha256, metadata_url = metadata_url, yanked = yanked, @@ -162,7 +155,7 @@ def parse_simpleapi_html(*, content, parse_index = False): return struct( sdists = sdists, whls = whls, - sha256s_by_version = sha256s_by_version, + hashes_by_version = hashes_by_version, ) def _parse_attrs(attr_string): diff --git a/python/private/pypi/pypi_cache.bzl b/python/private/pypi/pypi_cache.bzl index 28784a6a7b..4f6fa9ac37 100644 --- a/python/private/pypi/pypi_cache.bzl +++ b/python/private/pypi/pypi_cache.bzl @@ -13,7 +13,10 @@ load(":version_from_filename.bzl", "version_from_filename") # This value should be changed whenever the storage format changes. # Changing it simply means the information cached in the lockfile has to be # recomputed. -_FACT_VERSION = "v1" +# +# v2: the `dist_hashes` values and the `dist_yanked` keys are canonical +# `:` strings instead of bare sha256 hex digests. +_FACT_VERSION = "v2" def pypi_cache(mctx = None, store = None): """The cache for PyPI index queries. @@ -135,23 +138,23 @@ def _filter_packages(dists, requested_versions): } return result if result else None - sha256s_by_version = {} + hashes_by_version = {} whls = {} sdists = {} - for sha256, d in dists.sdists.items(): + for digest, d in dists.sdists.items(): if d.version not in requested_versions: continue - sdists[sha256] = d - sha256s_by_version.setdefault(d.version, []).append(sha256) + sdists[digest] = d + hashes_by_version.setdefault(d.version, []).append(digest) - for sha256, d in dists.whls.items(): + for digest, d in dists.whls.items(): if d.version not in requested_versions: continue - whls[sha256] = d - sha256s_by_version.setdefault(d.version, []).append(sha256) + whls[digest] = d + hashes_by_version.setdefault(d.version, []).append(digest) if not whls and not sdists: # TODO @aignas 2026-03-08: add logging @@ -161,9 +164,9 @@ def _filter_packages(dists, requested_versions): return struct( whls = whls, sdists = sdists, - sha256s_by_version = { + hashes_by_version = { k: sorted(v) - for k, v in sha256s_by_version.items() + for k, v in hashes_by_version.items() }, ) @@ -232,20 +235,14 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver retrieved_versions = {} - for url, stored_hash in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): - # The values are either bare sha256 hex digests or `:` when the - # index advertises a digest calculated with a different hash algorithm. - algo, sep, digest = stored_hash.partition(":") - if not sep: - algo, digest = "sha256", stored_hash - + for url, digest in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(url) if not filename: _, _, filename = url.rpartition("/") version = version_from_filename(filename) if version not in requested_versions: - # TODO @aignas 2026-01-21: do the check by requested shas at some point + # TODO @aignas 2026-01-21: do the check by requested digests at some point # We don't have sufficient info in the lock file, need to call the API # continue @@ -257,17 +254,16 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver else: dists = known_sources.setdefault("sdists", {}) - known_sources.setdefault("sha256s_by_version", {}).setdefault(version, []).append(digest) + known_sources.setdefault("hashes_by_version", {}).setdefault(version, []).append(digest) dists.setdefault(digest, struct( - sha256 = digest if algo == "sha256" else "", - hashes = {algo: digest} if digest else {}, + digest = digest, filename = filename, version = version, metadata_url = "", metadata_sha256 = "", url = url, - yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(stored_hash), + yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(digest), )) if not known_sources: @@ -282,9 +278,9 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver output = struct( whls = known_sources.get("whls", {}), sdists = known_sources.get("sdists", {}), - sha256s_by_version = { + hashes_by_version = { k: sorted(v) - for k, v in known_sources.get("sha256s_by_version", {}).items() + for k, v in known_sources.get("hashes_by_version", {}).items() }, ) @@ -325,9 +321,8 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_hashes": { # "": { # "": { - # # A bare sha256 hex digest or ":" when the index - # # advertises a digest calculated with a different hash algorithm. - # "": "", + # # An empty string if the index does not advertise any digest. + # "": ":", # }, # }, # }, @@ -341,23 +336,16 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_yanked": { # "": { # "": { - # "": "", # if the package is yanked + # ":": "", # if the package is yanked # }, # }, # }, # }, for digest, d in (value.sdists | value.whls).items(): - stored_hash = digest - if digest and d.sha256 != digest: - for algo, h in d.hashes.items(): - if h == digest: - stored_hash = "{}:{}".format(algo, digest) - break - - facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, stored_hash) + facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, digest) if not d.url.endswith(d.filename): facts.setdefault("dist_filenames", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, d.filename) if d.yanked != None: - facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(stored_hash, d.yanked) + facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(digest, d.yanked) return value diff --git a/python/private/pypi/simpleapi_download.bzl b/python/private/pypi/simpleapi_download.bzl index 5377a08093..b5f7032b0a 100644 --- a/python/private/pypi/simpleapi_download.bzl +++ b/python/private/pypi/simpleapi_download.bzl @@ -34,7 +34,7 @@ def simpleapi_download( """Download Simple API HTML. First it queries all of the indexes for available packages and then it downloads the contents of - the per-package URLs and sha256 values. This is to enable us to use bazel_downloader with + the per-package URLs and hash digest values. This is to enable us to use bazel_downloader with `requirements.txt` files. As a side effect we also are able to "cross-compile" by fetching the right wheel for the right target platform through the information that we retrieve here. @@ -294,6 +294,6 @@ def _with_index_url(index_url, values): return struct( sdists = values.sdists, whls = values.whls, - sha256s_by_version = values.sha256s_by_version, + hashes_by_version = values.hashes_by_version, index_url = index_url, ) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 8e5db5c3df..48b7772278 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -576,9 +576,8 @@ For example if your whl depends on `numpy` and your Python package repo is named "integrity": attr.string( doc = """\ The expected checksum of the downloaded whl in Subresource Integrity format -(e.g. `sha512-...`). Only used when `urls` is passed and `sha256` is empty, -e.g. when the requirements have been locked against an index that advertises -digests using a hash algorithm other than sha256. +(e.g. `sha256-...` or `sha512-...`). Only used when `urls` is passed. If +`sha256` is also set, it takes precedence over this attribute. :::{versionadded} VERSION_NEXT_FEATURE ::: diff --git a/python/private/pypi/whl_repo_name.bzl b/python/private/pypi/whl_repo_name.bzl index 29d774c361..739ff2a4a1 100644 --- a/python/private/pypi/whl_repo_name.bzl +++ b/python/private/pypi/whl_repo_name.bzl @@ -18,25 +18,31 @@ load("//python/private:normalize_name.bzl", "normalize_name") load(":parse_whl_name.bzl", "parse_whl_name") -def whl_repo_name(filename, sha256, *target_platforms): +def whl_repo_name(filename, digest, *target_platforms): """Return a valid whl_library repo name given a distribution filename. Args: filename: {type}`str` the filename of the distribution. - sha256: {type}`str` the sha256 of the distribution. + digest: {type}`str` the digest of the distribution, either as a + canonical `:` string or a bare hex digest. *target_platforms: {type}`list[str]` the extra suffixes to append. Only used when we need to support different extras per version. Returns: a string that can be used in {obj}`whl_library`. """ + + # Strip the `:` prefix so that the name only contains the hex digest + # and stays the same as it has always been for bare sha256 values. + digest = digest.rpartition(":")[2] + parts = [] if not filename.endswith(".whl"): # Then the filename is basically foo-3.2.1. name, _, tail = filename.rpartition("-") parts.append(normalize_name(name)) - if sha256: + if digest: parts.append("sdist") version = "" else: @@ -56,8 +62,8 @@ def whl_repo_name(filename, sha256, *target_platforms): parts.append(abi_tag) parts.append(platform_tag) - if sha256: - parts.append(sha256[:8]) + if digest: + parts.append(digest[:8]) elif version: parts.insert(1, version) diff --git a/tests/pypi/hash/BUILD.bazel b/tests/pypi/hash/BUILD.bazel new file mode 100644 index 0000000000..1e052af141 --- /dev/null +++ b/tests/pypi/hash/BUILD.bazel @@ -0,0 +1,3 @@ +load(":hash_tests.bzl", "hash_test_suite") + +hash_test_suite(name = "hash_tests") diff --git a/tests/pypi/hash/hash_tests.bzl b/tests/pypi/hash/hash_tests.bzl new file mode 100644 index 0000000000..6866a79d07 --- /dev/null +++ b/tests/pypi/hash/hash_tests.bzl @@ -0,0 +1,102 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:hash.bzl", "hash") # buildifier: disable=bzl-visibility + +_tests = [] + +# The digests of b"rules_python" with the SRI values calculated with: +# python3 -c 'import base64, hashlib; h = hashlib.sha512(b"rules_python").digest(); print(base64.b64encode(h))' +_SHA256 = "a90bb21ddf552d508565aa2f0d78ac13297e6c407e39eb578eeac09e62c5da3a" +_SHA256_SRI = "sha256-qQuyHd9VLVCFZaovDXisEyl+bEB+OetXjurAnmLF2jo=" +_SHA384 = "7407ad30f83623cabf9139bd80edb332516880321ab2a20fa0f281767675bfc10b53555eb5042aa58de4b9345f5f70b6" +_SHA384_SRI = "sha384-dAetMPg2I8q/kTm9gO2zMlFogDIasqIPoPKBdnZ1v8ELU1VetQQqpY3kuTRfX3C2" +_SHA512 = "127af31fba52ce5ac96a4bc0f1b31c31c12367feace2b7dbb81066df479883add9dd6166495b126b76ac8c624d254b6a4ba3fbb628e8dcf0a34e1b0b065f619f" +_SHA512_SRI = "sha512-EnrzH7pSzlrJakvA8bMcMcEjZ/6s4rfbuBBm30eYg63Z3WFmSVsSa3asjGJNJUtqS6P7tijo3PCjThsLBl9hnw==" + +def _test_digest(env): + env.expect.that_str(hash.digest("sha256", _SHA256)).equals("sha256:" + _SHA256) + + # The algorithm name is normalized to lower case. + env.expect.that_str(hash.digest("SHA256", _SHA256)).equals("sha256:" + _SHA256) + + # Unknown algorithms and empty digests yield nothing. + env.expect.that_str(hash.digest("egg", "foo")).equals("") + env.expect.that_str(hash.digest("sha256", "")).equals("") + env.expect.that_str(hash.digest("", "")).equals("") + +_tests.append(_test_digest) + +def _test_hex_to_sri(env): + env.expect.that_str(hash.hex_to_sri("sha256", _SHA256)).equals(_SHA256_SRI) + env.expect.that_str(hash.hex_to_sri("sha384", _SHA384)).equals(_SHA384_SRI) + env.expect.that_str(hash.hex_to_sri("sha512", _SHA512)).equals(_SHA512_SRI) + + # Upper case digests are accepted. + env.expect.that_str(hash.hex_to_sri("sha512", _SHA512.upper())).equals(_SHA512_SRI) + + # Algorithms that SRI does not support yield nothing. + env.expect.that_str(hash.hex_to_sri("md5", "0" * 32)).equals("") + env.expect.that_str(hash.hex_to_sri("blake2b", "0" * 128)).equals("") + + # Invalid digests yield nothing. + env.expect.that_str(hash.hex_to_sri("sha256", "")).equals("") + env.expect.that_str(hash.hex_to_sri("sha256", "abc")).equals("") + env.expect.that_str(hash.hex_to_sri("sha256", "not-hex!")).equals("") + +_tests.append(_test_hex_to_sri) + +def _test_integrity(env): + env.expect.that_str(hash.integrity("")).equals("") + env.expect.that_str(hash.integrity("md5:" + "0" * 32)).equals("") + env.expect.that_str(hash.integrity("sha256:" + _SHA256)).equals(_SHA256_SRI) + env.expect.that_str(hash.integrity("sha384:" + _SHA384)).equals(_SHA384_SRI) + env.expect.that_str(hash.integrity("sha512:" + _SHA512)).equals(_SHA512_SRI) + +_tests.append(_test_integrity) + +def _test_preferred_digest(env): + env.expect.that_str(hash.preferred_digest([])).equals("") + env.expect.that_str(hash.preferred_digest([""])).equals("") + + # sha256 stays preferred so that the repo names remain stable. + env.expect.that_str(hash.preferred_digest([ + "sha256:" + _SHA256, + "sha512:" + _SHA512, + ])).equals("sha256:" + _SHA256) + + # Otherwise the strongest SRI supported algorithm wins. + env.expect.that_str(hash.preferred_digest([ + "sha384:" + _SHA384, + "sha512:" + _SHA512, + ])).equals("sha512:" + _SHA512) + + # And anything else is picked alphabetically for determinism. + env.expect.that_str(hash.preferred_digest([ + "md5:deadb00f", + "blake2b:deadbeef", + ])).equals("blake2b:deadbeef") + +_tests.append(_test_preferred_digest) + +def hash_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/hashes/BUILD.bazel b/tests/pypi/hashes/BUILD.bazel deleted file mode 100644 index 5c15a2b079..0000000000 --- a/tests/pypi/hashes/BUILD.bazel +++ /dev/null @@ -1,3 +0,0 @@ -load(":hashes_tests.bzl", "hashes_test_suite") - -hashes_test_suite(name = "hashes_tests") diff --git a/tests/pypi/hashes/hashes_tests.bzl b/tests/pypi/hashes/hashes_tests.bzl deleted file mode 100644 index 2d4c92e737..0000000000 --- a/tests/pypi/hashes/hashes_tests.bzl +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2026 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"" - -load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private/pypi:hashes.bzl", "hex_to_sri", "integrity_from_hashes", "preferred_digest") # buildifier: disable=bzl-visibility - -_tests = [] - -# The digests of b"rules_python" with the SRI values calculated with: -# python3 -c 'import base64, hashlib; h = hashlib.sha512(b"rules_python").digest(); print(base64.b64encode(h))' -_SHA256 = "a90bb21ddf552d508565aa2f0d78ac13297e6c407e39eb578eeac09e62c5da3a" -_SHA256_SRI = "sha256-qQuyHd9VLVCFZaovDXisEyl+bEB+OetXjurAnmLF2jo=" -_SHA384 = "7407ad30f83623cabf9139bd80edb332516880321ab2a20fa0f281767675bfc10b53555eb5042aa58de4b9345f5f70b6" -_SHA384_SRI = "sha384-dAetMPg2I8q/kTm9gO2zMlFogDIasqIPoPKBdnZ1v8ELU1VetQQqpY3kuTRfX3C2" -_SHA512 = "127af31fba52ce5ac96a4bc0f1b31c31c12367feace2b7dbb81066df479883add9dd6166495b126b76ac8c624d254b6a4ba3fbb628e8dcf0a34e1b0b065f619f" -_SHA512_SRI = "sha512-EnrzH7pSzlrJakvA8bMcMcEjZ/6s4rfbuBBm30eYg63Z3WFmSVsSa3asjGJNJUtqS6P7tijo3PCjThsLBl9hnw==" - -def _test_hex_to_sri(env): - env.expect.that_str(hex_to_sri("sha256", _SHA256)).equals(_SHA256_SRI) - env.expect.that_str(hex_to_sri("sha384", _SHA384)).equals(_SHA384_SRI) - env.expect.that_str(hex_to_sri("sha512", _SHA512)).equals(_SHA512_SRI) - - # Upper case digests are accepted. - env.expect.that_str(hex_to_sri("sha512", _SHA512.upper())).equals(_SHA512_SRI) - - # Algorithms that SRI does not support yield nothing. - env.expect.that_str(hex_to_sri("md5", "0" * 32)).equals("") - env.expect.that_str(hex_to_sri("blake2b", "0" * 128)).equals("") - - # Invalid digests yield nothing. - env.expect.that_str(hex_to_sri("sha256", "")).equals("") - env.expect.that_str(hex_to_sri("sha256", "abc")).equals("") - env.expect.that_str(hex_to_sri("sha256", "not-hex!")).equals("") - -_tests.append(_test_hex_to_sri) - -def _test_integrity_from_hashes(env): - env.expect.that_str(integrity_from_hashes({})).equals("") - env.expect.that_str(integrity_from_hashes({"md5": "0" * 32})).equals("") - env.expect.that_str(integrity_from_hashes({"sha512": _SHA512})).equals(_SHA512_SRI) - - # The strongest algorithm wins. - env.expect.that_str(integrity_from_hashes({ - "sha256": _SHA256, - "sha384": _SHA384, - "sha512": _SHA512, - })).equals(_SHA512_SRI) - env.expect.that_str(integrity_from_hashes({ - "sha256": _SHA256, - "sha384": _SHA384, - })).equals(_SHA384_SRI) - -_tests.append(_test_integrity_from_hashes) - -def _test_preferred_digest(env): - env.expect.that_str(preferred_digest({})).equals("") - env.expect.that_str(preferred_digest({"sha256": _SHA256, "sha512": _SHA512})).equals(_SHA256) - env.expect.that_str(preferred_digest({"sha384": _SHA384, "sha512": _SHA512})).equals(_SHA512) - env.expect.that_str(preferred_digest({"blake2b": "deadbeef", "md5": "deadb00f"})).equals("deadbeef") - -_tests.append(_test_preferred_digest) - -def hashes_test_suite(name): - """Create the test suite. - - Args: - name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index 0fc51e8e7d..f421c7e351 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -340,8 +340,8 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://example.com/simple/", + "integrity": "sha256-3q2+7w==", "requirement": "simple[foo]==0.0.1", - "sha256": "deadbeef", "urls": ["/simple-0.0.1-py3-none-any.whl"], }, "pypi_315_simple_py3_none_any_deadbeef_windows_aarch64": { @@ -349,8 +349,8 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://example.com/simple/", + "integrity": "sha256-3q2+7w==", "requirement": "simple==0.0.1", - "sha256": "deadbeef", "urls": ["/simple-0.0.1-py3-none-any.whl"], }, }) @@ -423,7 +423,6 @@ def _test_simple_sha512_simpleapi(env): "index_url": "https://example.com/simple/", "integrity": "sha512-3q2+7w==", "requirement": "simple==0.0.1", - "sha256": "", "urls": ["/simple-0.0.1-py3-none-any.whl"], }, }) @@ -744,8 +743,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-iADe7wAmAR1QLAwlbMS2fQAjR/Y8OjjNjkXx9EXGE2Q=", "requirement": "torch==2.4.1+cpu", - "sha256": "8800deef0026011d502c0c256cc4b67d002347f63c3a38cd8e45f1f445c61364", "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-linux_x86_64.whl"], }, "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432_linux_aarch64": { @@ -753,8 +752,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-NhCUMrEL1xY8mzDOiW88LMobhrl2X5VqFZTw/0MJHio=", "requirement": "torch==2.4.1", - "sha256": "36109432b10bd7163c9b30ce896f3c2cca1b86b9765f956a1594f0ff43091e2a", "urls": ["/whl/cpu/torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"], }, "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c_windows_x86_64": { @@ -762,8 +761,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-OlcOXFU0Fc293+Z5IHMns6OAayHGreoU+6d2hNFhnpc=", "requirement": "torch==2.4.1+cpu", - "sha256": "3a570e5c553415cdbddfe679207327b3a3806b21c6adea14fba77684d1619e97", "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-win_amd64.whl"], }, "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5_osx_aarch64": { @@ -771,8 +770,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-crSE1bbOwac1vz+locSIPQF0hpjF6c/b60/6t8eYfg0=", "requirement": "torch==2.4.1", - "sha256": "72b484d5b6cec1a735bf3fa5a1c4883d01748698c5e9cfdbeb4ffab7c7987e0d", "urls": ["/whl/cpu/torch-2.4.1-cp312-none-macosx_11_0_arm64.whl"], }, }) @@ -859,16 +858,15 @@ simple==0.0.1 --hash=sha256:deadb00f return { "simple": struct( whls = { - "deadb00f": struct( + "sha256:deadb00f": struct( yanked = None, filename = "simple-0.0.1-py3-none-any.whl", - sha256 = "deadb00f", - hashes = {"sha256": "deadb00f"}, + digest = "sha256:deadb00f", url = test.expect_url, ), }, sdists = {}, - sha256s_by_version = {}, + hashes_by_version = {}, index_url = test.expect_index_url, ), } @@ -915,8 +913,8 @@ simple==0.0.1 --hash=sha256:deadb00f "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": test.expect_index_url, + "integrity": "sha256-3q2wDw==", "requirement": "simple==0.0.1", - "sha256": "deadb00f", "urls": [test.expect_url], } if getattr(test, "envsubst", []): @@ -1050,36 +1048,33 @@ def _test_simple_get_index(env): return { "plat_pkg": struct( whls = { - "deadb44f": struct( + "sha256:deadb44f": struct( yanked = None, filename = "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", - sha256 = "deadb44f", - hashes = {"sha256": "deadb44f"}, + digest = "sha256:deadb44f", url = "example2.org/index/plat_pkg/", ), }, sdists = {}, - sha256s_by_version = { - "0.0.4": ["deadb44f"], + hashes_by_version = { + "0.0.4": ["sha256:deadb44f"], }, index_url = "https://pypi.org/simple", ), "simple": struct( whls = { - "deadb00f": struct( + "sha256:deadb00f": struct( yanked = None, filename = "simple-0.0.1-py3-none-any.whl", - sha256 = "deadb00f", - hashes = {"sha256": "deadb00f"}, + digest = "sha256:deadb00f", url = "example2.org", ), }, sdists = { - "deadbeef": struct( + "sha256:deadbeef": struct( yanked = None, filename = "simple-0.0.1.tar.gz", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "example.org", ), }, @@ -1087,18 +1082,17 @@ def _test_simple_get_index(env): ), "some_other_pkg": struct( whls = { - "deadb33f": struct( + "sha256:deadb33f": struct( yanked = None, filename = "some-other-pkg-0.0.1-py3-none-any.whl", - sha256 = "deadb33f", - hashes = {"sha256": "deadb33f"}, + digest = "sha256:deadb33f", url = "example2.org/index/some_other_pkg/", ), }, sdists = {}, - sha256s_by_version = { - "0.0.1": ["deadb33f"], - "0.0.3": ["deadbeef"], + hashes_by_version = { + "0.0.1": ["sha256:deadb33f"], + "0.0.3": ["sha256:deadbeef"], }, index_url = "https://with_index_url", ), @@ -1263,17 +1257,17 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", + "integrity": "", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_sdist_without_sha @ some-archive/any-name.tar.gz", - "sha256": "", "urls": ["some-archive/any-name.tar.gz"], }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "filename": "direct_without_sha-0.0.1-py3-none-any.whl", + "integrity": "", "requirement": "direct_without_sha==0.0.1", - "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], "whl_patches": {"my_patch": "1"}, }, @@ -1296,8 +1290,8 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", "index_url": "https://pypi.org/simple", + "integrity": "sha256-3q20Tw==", "requirement": "plat_pkg==0.0.4", - "sha256": "deadb44f", "urls": ["example2.org/index/plat_pkg/"], }, "pypi_315_simple_py3_none_any_deadb00f": { @@ -1305,16 +1299,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://pypi.org/simple", + "integrity": "sha256-3q2wDw==", "requirement": "simple==0.0.1", - "sha256": "deadb00f", "urls": ["example2.org"], }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "filename": "some_pkg-0.0.1-py3-none-any.whl", + "integrity": "sha256-3q26rw==", "requirement": "some_pkg==0.0.1", - "sha256": "deadbaaf", "urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"], }, "pypi_315_some_py3_none_any_deadb33f": { @@ -1322,8 +1316,8 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "index_url": "https://with_index_url", + "integrity": "sha256-3q2zPw==", "requirement": "some_other_pkg==0.0.1", - "sha256": "deadb33f", "urls": ["example2.org/index/some_other_pkg/"], }, }) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 76f93999f8..54f906929a 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -196,8 +196,7 @@ def _test_simple(env): ], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -226,8 +225,7 @@ def _test_direct_urls_integration(env): extra_pip_args = [], filename = "foo-1.1.tar.gz", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["osx_x86_64"], url = "https://github.com/org/foo/downloads/foo-1.1.tar.gz", yanked = None, @@ -237,8 +235,7 @@ def _test_direct_urls_integration(env): extra_pip_args = [], filename = "package.whl", requirement_line = "foo[extra]", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["linux_x86_64"], url = "https://some-url/package.whl", yanked = None, @@ -270,8 +267,7 @@ def _test_direct_urls_no_extract(env): extra_pip_args = [], filename = "", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["osx_x86_64"], url = "", yanked = None, @@ -281,8 +277,7 @@ def _test_direct_urls_no_extract(env): extra_pip_args = [], filename = "", requirement_line = "foo[extra] @ https://some-url/package.whl", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["linux_x86_64"], url = "", yanked = None, @@ -317,8 +312,7 @@ def _test_extra_pip_args(env): ], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -348,8 +342,7 @@ def _test_dupe_requirements(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -381,8 +374,7 @@ def _test_multi_os(env): target_platforms = ["windows_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -400,8 +392,7 @@ def _test_multi_os(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), struct( @@ -411,8 +402,7 @@ def _test_multi_os(env): target_platforms = ["windows_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -450,8 +440,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -469,8 +458,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), struct( @@ -480,8 +468,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_osx_aarch64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -539,8 +526,7 @@ def _test_env_marker_resolution(env): target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -558,8 +544,7 @@ def _test_env_marker_resolution(env): target_platforms = ["cp311_windows_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -590,8 +575,7 @@ def _test_different_package_version(env): target_platforms = ["linux_aarch64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), struct( @@ -601,8 +585,7 @@ def _test_different_package_version(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -633,8 +616,7 @@ def _test_different_package_extras(env): target_platforms = ["linux_aarch64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), struct( @@ -644,8 +626,7 @@ def _test_different_package_extras(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -675,8 +656,7 @@ def _test_optional_hash(env): target_platforms = ["linux_x86_64"], url = "https://example.org/bar-0.0.4.whl", filename = "bar-0.0.4.whl", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -694,8 +674,7 @@ def _test_optional_hash(env): target_platforms = ["linux_x86_64"], url = "https://example.org/foo-0.0.5.whl", filename = "foo-0.0.5.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", yanked = None, ), ], @@ -725,8 +704,7 @@ def _test_git_sources(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", - hashes = {}, + digest = "", yanked = None, ), ], @@ -766,26 +744,23 @@ def _test_overlapping_shas_with_index_results(env): "foo": struct( index_url = "https://example.com", sdists = { - "5d15t": struct( + "sha256:5d15t": struct( url = "sdist", - sha256 = "5d15t", - hashes = {"sha256": "5d15t"}, + digest = "sha256:5d15t", filename = "foo-0.0.1.tar.gz", yanked = None, ), }, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", yanked = None, ), - "deadbaaf": struct( + "sha256:deadbaaf": struct( url = "super2", - sha256 = "deadbaaf", - hashes = {"sha256": "deadbaaf"}, + digest = "sha256:deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -806,8 +781,7 @@ def _test_overlapping_shas_with_index_results(env): extra_pip_args = [], filename = "foo-0.0.1-py3-none-any.whl", requirement_line = "foo==0.0.3", - sha256 = "deadbaaf", - hashes = {"sha256": "deadbaaf"}, + digest = "sha256:deadbaaf", target_platforms = ["cp39_linux_x86_64"], url = "super2", yanked = None, @@ -817,8 +791,7 @@ def _test_overlapping_shas_with_index_results(env): extra_pip_args = [], filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", requirement_line = "foo==0.0.3", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", target_platforms = ["cp39_osx_x86_64"], url = "super2", yanked = None, @@ -851,10 +824,9 @@ def _test_non_sha256_hash_matching(env): index_url = "https://example.com", sdists = {}, whls = { - "deadbeef": struct( + "sha512:deadbeef": struct( url = "https://example.com/foo-0.0.1-py3-none-any.whl", - sha256 = "", - hashes = {"sha512": "deadbeef"}, + digest = "sha512:deadbeef", filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -875,8 +847,7 @@ def _test_non_sha256_hash_matching(env): extra_pip_args = [], filename = "foo-0.0.1-py3-none-any.whl", requirement_line = "foo==0.0.1", - sha256 = "", - hashes = {"sha512": "deadbeef"}, + digest = "sha512:deadbeef", target_platforms = ["cp311_linux_x86_64"], url = "https://example.com/foo-0.0.1-py3-none-any.whl", yanked = None, @@ -921,17 +892,15 @@ def _test_get_index_urls_different_versions(env): index_url = "", sdists = {}, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), - "deadbaaf": struct( + "sha256:deadbaaf": struct( url = "super2", - sha256 = "deadbaaf", - hashes = {"sha256": "deadbaaf"}, + digest = "sha256:deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -952,8 +921,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "", requirement_line = "boo==0.0.4 --hash=sha256:deadbaaf", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -971,8 +939,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "", requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", - sha256 = "", - hashes = {}, + digest = "", target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -982,8 +949,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -1063,10 +1029,9 @@ def _test_get_index_urls_single_py_version(env): index_url = "", sdists = {}, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), @@ -1087,8 +1052,7 @@ def _test_get_index_urls_single_py_version(env): extra_pip_args = [], filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -1158,8 +1122,7 @@ def _test_uv_lock_consistent(env): requirement_line = "foo[extra]==0.0.1", target_platforms = ["linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1187,8 +1150,7 @@ def _test_uv_lock_primary_source(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1216,8 +1178,7 @@ def _test_uv_lock_non_sha256_hash(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "", - hashes = {"sha512": "deadbeef"}, + digest = "sha512:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1245,8 +1206,7 @@ def _test_uv_lock_primary_source_multiple_versions(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1256,8 +1216,7 @@ def _test_uv_lock_primary_source_multiple_versions(env): requirement_line = "foo==0.0.2", target_platforms = ["linux_x86_64"], filename = "foo-0.0.2-py3-none-any.whl", - sha256 = "deadb11f", - hashes = {"sha256": "deadb11f"}, + digest = "sha256:deadb11f", url = "https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl", yanked = None, ), @@ -1285,8 +1244,7 @@ def _test_uv_lock_primary_source_with_extras(env): requirement_line = "foo[extra]==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1314,8 +1272,7 @@ def _test_uv_lock_primary_source_includes_virtual(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1353,8 +1310,7 @@ def _test_uv_lock_cross_consistent(env): requirement_line = "foo[extra]==0.0.1", target_platforms = ["linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1382,8 +1338,7 @@ def _test_uv_lock_vcs_entry(env): requirement_line = "foo==0.1.0", target_platforms = ["linux_x86_64"], filename = "foo.git", - sha256 = "", - hashes = {}, + digest = "", url = "https://github.com/org/foo.git", yanked = None, ), @@ -1411,8 +1366,7 @@ def _test_uv_lock_rules_python_pkg_not_skipped(env): requirement_line = "rules_python==0.0.1", target_platforms = ["linux_x86_64"], filename = "rules_python-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1445,8 +1399,7 @@ def _test_uv_lock_no_consistency_check(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1474,8 +1427,7 @@ def _test_uv_lock_multiple_packages(env): requirement_line = "bar==0.0.1", target_platforms = ["linux_x86_64"], filename = "bar-0.0.1.tar.gz", - sha256 = "deadb00f", - hashes = {"sha256": "deadb00f"}, + digest = "sha256:deadb00f", url = "https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz", yanked = None, ), @@ -1493,8 +1445,7 @@ def _test_uv_lock_multiple_packages(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1523,8 +1474,7 @@ def _test_uv_lock_with_extra_pip_args(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1556,8 +1506,7 @@ def _test_uv_lock_multi_os_with_requirements(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_aarch64", "linux_x86_64", "windows_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1585,8 +1534,7 @@ def _test_uv_lock_extras_optional_deps(env): requirement_line = "foo[extra1,extra2]==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1614,8 +1562,7 @@ def _test_uv_lock_extras_dep_edge(env): requirement_line = "bar[extra1]==0.0.2", target_platforms = ["linux_x86_64"], filename = "bar-0.0.2-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl", yanked = None, ), @@ -1633,8 +1580,7 @@ def _test_uv_lock_extras_dep_edge(env): requirement_line = "foo==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "baadbeef", - hashes = {"sha256": "baadbeef"}, + digest = "sha256:baadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -1669,8 +1615,7 @@ def _test_uv_lock_wheel_dedup_single_version(env): requirement_line = "foo==0.0.1", target_platforms = ["cp39_linux_x86_64"], filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", - sha256 = "aaa", - hashes = {"sha256": "aaa"}, + digest = "sha256:aaa", url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", yanked = None, ), @@ -1716,8 +1661,7 @@ def _test_uv_lock_wheel_dedup_resolution_markers(env): requirement_line = "foo==0.0.1", target_platforms = ["cp39_linux_x86_64"], filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", - sha256 = "aaa", - hashes = {"sha256": "aaa"}, + digest = "sha256:aaa", url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", yanked = None, ), @@ -1727,8 +1671,7 @@ def _test_uv_lock_wheel_dedup_resolution_markers(env): requirement_line = "foo==0.0.2", target_platforms = ["cp39_osx_aarch64"], filename = "foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", - sha256 = "ccc", - hashes = {"sha256": "ccc"}, + digest = "sha256:ccc", url = "https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", yanked = None, ), @@ -1756,8 +1699,7 @@ def _test_uv_lock_requires_dist_extras(env): requirement_line = "foo[all]==0.0.1", target_platforms = ["linux_x86_64"], filename = "foo-0.0.1-py3-none-any.whl", - sha256 = "deadbeef", - hashes = {"sha256": "deadbeef"}, + digest = "sha256:deadbeef", url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", yanked = None, ), diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index 022184a242..975fcd0ee6 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -78,7 +78,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", yanked = None, version = "0.0.1", @@ -95,7 +95,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", yanked = "", @@ -112,7 +112,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", # NOTE @aignas 2026-03-09: we preserve the white space @@ -130,7 +130,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", yanked = "", @@ -143,22 +143,22 @@ def _test_sdist(env): got = parse_simpleapi_html(content = html) env.expect.that_collection(got.sdists).has_size(1) env.expect.that_collection(got.whls).has_size(0) - env.expect.that_collection(got.sha256s_by_version).has_size(1) + env.expect.that_collection(got.hashes_by_version).has_size(1) if not got: fail("expected at least one element, but did not get anything from:\n{}".format(html)) actual = env.expect.that_struct( - got.sdists[want.sha256], + got.sdists[want.digest], attrs = dict( filename = subjects.str, - sha256 = subjects.str, + digest = subjects.str, url = subjects.str, yanked = subjects.str, version = subjects.str, ), ) actual.filename().equals(want.filename) - actual.sha256().equals(want.sha256) + actual.digest().equals(want.digest) actual.url().equals(want.url) actual.yanked().equals(want.yanked) actual.version().equals(want.version) @@ -182,7 +182,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -202,7 +202,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -221,7 +221,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", yanked = None, @@ -240,7 +240,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", yanked = None, @@ -258,7 +258,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "", metadata_url = "", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -275,12 +275,12 @@ def _test_whls(env): fail("expected at least one element, but did not get anything from:\n{}".format(html)) actual = env.expect.that_struct( - got.whls[want.sha256], + got.whls[want.digest], attrs = dict( filename = subjects.str, metadata_sha256 = subjects.str, metadata_url = subjects.str, - sha256 = subjects.str, + digest = subjects.str, url = subjects.str, yanked = subjects.str, version = subjects.str, @@ -289,7 +289,7 @@ def _test_whls(env): actual.filename().equals(want.filename) actual.metadata_sha256().equals(want.metadata_sha256) actual.metadata_url().equals(want.metadata_url) - actual.sha256().equals(want.sha256) + actual.digest().equals(want.digest) actual.url().equals(want.url) actual.yanked().equals(want.yanked) actual.version().equals(want.version) @@ -324,27 +324,25 @@ def _test_non_sha256_fragment(env): env.expect.that_collection(got.whls).has_size(2) env.expect.that_collection(got.sdists).has_size(1) - env.expect.that_dict(got.sha256s_by_version).contains_exactly({ - "0.0.1": ["deadbeef", "deadb00f"], + env.expect.that_dict(got.hashes_by_version).contains_exactly({ + "0.0.1": ["sha512:deadbeef", "sha512:deadb00f"], "0.0.2": [""], }) - whl = got.whls["deadbeef"] - env.expect.that_str(whl.sha256).equals("") - env.expect.that_dict(whl.hashes).contains_exactly({"sha512": "deadbeef"}) + whl = got.whls["sha512:deadbeef"] + env.expect.that_str(whl.digest).equals("sha512:deadbeef") env.expect.that_str(whl.url).equals("https://example.org/full-url/foo-0.0.1-py3-none-any.whl") - sdist = got.sdists["deadb00f"] - env.expect.that_str(sdist.sha256).equals("") - env.expect.that_dict(sdist.hashes).contains_exactly({"sha512": "deadb00f"}) + sdist = got.sdists["sha512:deadb00f"] + env.expect.that_str(sdist.digest).equals("sha512:deadb00f") no_hash_whl = got.whls[""] - env.expect.that_dict(no_hash_whl.hashes).contains_exactly({}) + env.expect.that_str(no_hash_whl.digest).equals("") env.expect.that_str(no_hash_whl.url).equals("https://example.org/full-url/foo-0.0.2-py3-none-any.whl#egg=foo") _tests.append(_test_non_sha256_fragment) -def _test_sha256_fragment_hashes(env): +def _test_sha256_fragment_digest(env): html = _generate_html( struct( attrs = [ @@ -355,11 +353,10 @@ def _test_sha256_fragment_hashes(env): ) got = parse_simpleapi_html(content = html) - whl = got.whls["deadbeef"] - env.expect.that_str(whl.sha256).equals("deadbeef") - env.expect.that_dict(whl.hashes).contains_exactly({"sha256": "deadbeef"}) + whl = got.whls["sha256:deadbeef"] + env.expect.that_str(whl.digest).equals("sha256:deadbeef") -_tests.append(_test_sha256_fragment_hashes) +_tests.append(_test_sha256_fragment_digest) def parse_simpleapi_html_test_suite(name): """Create the test suite. diff --git a/tests/pypi/pypi_cache/pypi_cache_tests.bzl b/tests/pypi/pypi_cache/pypi_cache_tests.bzl index fc77413664..21c2f5bd8c 100644 --- a/tests/pypi/pypi_cache/pypi_cache_tests.bzl +++ b/tests/pypi/pypi_cache/pypi_cache_tests.bzl @@ -11,8 +11,8 @@ def _cache(env, **kwargs): cache = pypi_cache(**kwargs) attrs = { + "hashes_by_version": subjects.dict, "sdists": subjects.dict, - "sha256s_by_version": subjects.dict, "whls": subjects.dict, } @@ -48,14 +48,14 @@ def _test_memory_cache_hit(env): # Mocked parsed result from a PyPI-like index fake_result = struct( sdists = { - "sha_1": struct(version = "1.0.0", filename = "pkg-1.0.0.tar.gz"), + "sha256:sha_1": struct(version = "1.0.0", filename = "pkg-1.0.0.tar.gz"), }, whls = { - "sha_2": struct(version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl"), + "sha256:sha_2": struct(version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl"), }, - sha256s_by_version = { - "1.0.0": ["sha_1"], - "1.1.0": ["sha_2"], + hashes_by_version = { + "1.0.0": ["sha256:sha_1"], + "1.1.0": ["sha256:sha_2"], }, ) @@ -70,7 +70,7 @@ def _test_memory_cache_hit(env): got.sdists().contains_exactly(fake_result.sdists) got.whls().contains_exactly(fake_result.whls) - got.sha256s_by_version().contains_exactly(fake_result.sha256s_by_version) + got.hashes_by_version().contains_exactly(fake_result.hashes_by_version) # A different key with fewer versions key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.0.0"]) @@ -78,7 +78,7 @@ def _test_memory_cache_hit(env): got = cache.get(key) got.sdists().contains_exactly(fake_result.sdists) got.whls().contains_exactly({}) - got.sha256s_by_version().contains_exactly({"1.0.0": ["sha_1"]}) + got.hashes_by_version().contains_exactly({"1.0.0": ["sha256:sha_1"]}) # A key with no matches key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.2.0"]) @@ -94,37 +94,34 @@ def _test_pypi_cache_writes_to_facts(env): fake_result = struct( sdists = { - "sha_sdist": struct( + "sha256:sha_sdist": struct( version = "1.0.0", filename = "pkg-1.0.0.tar.gz", url = "https://pypi.org/files/pkg-1.0.0.tar.gz", - sha256 = "sha_sdist", - hashes = {"sha256": "sha_sdist"}, + digest = "sha256:sha_sdist", yanked = "", ), }, whls = { - "sha_whl": struct( + "sha256:sha_whl": struct( version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", - sha256 = "sha_whl", - hashes = {"sha256": "sha_whl"}, + digest = "sha256:sha_whl", yanked = "Security issue", ), # This won't get stored - "sha_whl_2": struct( + "sha256:sha_whl_2": struct( version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl", - sha256 = "sha_whl_2", - hashes = {"sha256": "sha_whl_2"}, + digest = "sha256:sha_whl_2", yanked = None, ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist", "sha_whl"], - "1.1.0": ["sha_whl_2"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist", "sha256:sha_whl"], + "1.1.0": ["sha256:sha_whl_2"], }, ) @@ -136,11 +133,11 @@ def _test_pypi_cache_writes_to_facts(env): # Then the key returns us the same items got = cache.get(key) got.whls().contains_exactly({ - "sha_whl": fake_result.whls["sha_whl"], + "sha256:sha_whl": fake_result.whls["sha256:sha_whl"], }) got.sdists().contains_exactly(fake_result.sdists) - got.sha256s_by_version().contains_exactly({ - "1.0.0": fake_result.sha256s_by_version["1.0.0"], + got.hashes_by_version().contains_exactly({ + "1.0.0": fake_result.hashes_by_version["1.0.0"], }) # Then when we get facts at the end @@ -149,30 +146,30 @@ def _test_pypi_cache_writes_to_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) # When we get the other items cached in memory, they get written to facts got = cache.get((key[0], key[1], ["1.1.0"])) got.whls().contains_exactly({ - "sha_whl_2": fake_result.whls["sha_whl_2"], + "sha256:sha_whl_2": fake_result.whls["sha256:sha_whl_2"], }) got.sdists().contains_exactly({}) - got.sha256s_by_version().contains_exactly({ - "1.1.0": fake_result.sha256s_by_version["1.1.0"], + got.hashes_by_version().contains_exactly({ + "1.1.0": fake_result.hashes_by_version["1.1.0"], }) # Then when we get facts at the end @@ -181,21 +178,21 @@ def _test_pypi_cache_writes_to_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", - "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_2", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha256:sha_whl_2", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) _tests.append(_test_pypi_cache_writes_to_facts) @@ -207,20 +204,20 @@ def _test_pypi_cache_reads_from_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) cache = _cache(env, mctx = mock_ctx) @@ -235,9 +232,8 @@ def _test_pypi_cache_reads_from_facts(env): expected_result = struct( sdists = { - "sha_sdist": struct( - sha256 = "sha_sdist", - hashes = {"sha256": "sha_sdist"}, + "sha256:sha_sdist": struct( + digest = "sha256:sha_sdist", version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -247,9 +243,8 @@ def _test_pypi_cache_reads_from_facts(env): ), }, whls = { - "sha_whl": struct( - sha256 = "sha_whl", - hashes = {"sha256": "sha_whl"}, + "sha256:sha_whl": struct( + digest = "sha256:sha_whl", version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", @@ -258,14 +253,14 @@ def _test_pypi_cache_reads_from_facts(env): yanked = "Security issue", ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist", "sha_whl"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist", "sha256:sha_whl"], }, ) got.whls().contains_exactly(expected_result.whls) got.sdists().contains_exactly(expected_result.sdists) - got.sha256s_by_version().contains_exactly(expected_result.sha256s_by_version) + got.hashes_by_version().contains_exactly(expected_result.hashes_by_version) # Then when we store the same facts back again, because we accessed the cached keys. cache.get_facts().contains_exactly(mock_ctx.facts) @@ -283,14 +278,14 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): "dist_hashes": { "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", - "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_1.1.0", - "https://pypi.org/files/pkg-1.1.0.tar.gz": "sha_sdist_1.1.0", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist_1.0.0", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha256:sha_whl_1.1.0", + "https://pypi.org/files/pkg-1.1.0.tar.gz": "sha256:sha_sdist_1.1.0", }, }, }, - "fact_version": "v1", + "fact_version": "v2", }) cache = _cache(env, mctx = mock_ctx) @@ -300,9 +295,8 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): expected = struct( sdists = { - "sha_sdist_1.0.0": struct( - sha256 = "sha_sdist_1.0.0", - hashes = {"sha256": "sha_sdist_1.0.0"}, + "sha256:sha_sdist_1.0.0": struct( + digest = "sha256:sha_sdist_1.0.0", version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -312,9 +306,8 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): ), }, whls = { - "sha_whl_1.0.0": struct( - sha256 = "sha_whl_1.0.0", - hashes = {"sha256": "sha_whl_1.0.0"}, + "sha256:sha_whl_1.0.0": struct( + digest = "sha256:sha_whl_1.0.0", version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", metadata_url = "", @@ -323,25 +316,25 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): yanked = None, ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist_1.0.0", "sha_whl_1.0.0"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist_1.0.0", "sha256:sha_whl_1.0.0"], }, ) got.whls().contains_exactly(expected.whls) got.sdists().contains_exactly(expected.sdists) - got.sha256s_by_version().contains_exactly(expected.sha256s_by_version) + got.hashes_by_version().contains_exactly(expected.hashes_by_version) # get_facts() must only contain version 1.0.0 data; 1.1.0 is dropped cache.get_facts().contains_exactly({ "dist_hashes": { "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist_1.0.0", }, }, }, - "fact_version": "v1", + "fact_version": "v2", }) _tests.append(_test_pypi_cache_reads_from_facts_drops_unaccessed_dists) @@ -354,17 +347,16 @@ def _test_pypi_cache_facts_non_sha256_round_trip(env): fake_result = struct( sdists = {}, whls = { - "whl_digest": struct( + "sha512:whl_digest": struct( version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", - sha256 = "", - hashes = {"sha512": "whl_digest"}, + digest = "sha512:whl_digest", yanked = None, ), }, - sha256s_by_version = { - "1.0.0": ["whl_digest"], + hashes_by_version = { + "1.0.0": ["sha512:whl_digest"], }, ) @@ -380,7 +372,7 @@ def _test_pypi_cache_facts_non_sha256_round_trip(env): }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) # And a cache that only has the facts reconstructs the digests @@ -392,14 +384,13 @@ def _test_pypi_cache_facts_non_sha256_round_trip(env): }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version })) got = cache.get(key) got.whls().contains_exactly({ - "whl_digest": struct( - sha256 = "", - hashes = {"sha512": "whl_digest"}, + "sha512:whl_digest": struct( + digest = "sha512:whl_digest", version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", metadata_url = "", @@ -408,7 +399,7 @@ def _test_pypi_cache_facts_non_sha256_round_trip(env): yanked = None, ), }) - got.sha256s_by_version().contains_exactly({"1.0.0": ["whl_digest"]}) + got.hashes_by_version().contains_exactly({"1.0.0": ["sha512:whl_digest"]}) _tests.append(_test_pypi_cache_facts_non_sha256_round_trip) @@ -453,7 +444,7 @@ def _test_pypi_cache_writes_index_urls_to_facts(env): cache.setdefault(key, fake_result) cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -465,7 +456,7 @@ def _test_pypi_cache_writes_index_urls_to_facts(env): cache.setdefault(key, fake_result) cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -479,7 +470,7 @@ _tests.append(_test_pypi_cache_writes_index_urls_to_facts) def _test_pypi_cache_reads_index_urls_from_facts(env): """Verifies that reading index_urls from facts works correctly.""" mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -510,7 +501,7 @@ _tests.append(_test_pypi_cache_reads_index_urls_from_facts) def _test_pypi_cache_reads_index_urls_from_facts_incomplete(env): """Verifies that incomplete index_urls facts returns None (forces fresh download).""" mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -533,7 +524,7 @@ def _test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed(env): removed from all requirements files) get cleaned up from the lockfile. """ mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -554,7 +545,7 @@ def _test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed(env): # get_facts() must only return the requested (accessed) subset; pkg-c is dropped cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", diff --git a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl index 4e86b76e10..dfa33ebe65 100644 --- a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl +++ b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl @@ -43,7 +43,7 @@ def _test_simple(env): output = struct( sdists = {"deadbeef": url.strip("/").split("/")[-1]}, whls = {"deadb33f": url.strip("/").split("/")[-1]}, - sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + hashes_by_version = {"fizz": url.strip("/").split("/")[-1]}, ), success = True, ) @@ -71,19 +71,19 @@ def _test_simple(env): "bar": struct( index_url = "https://main.com/bar/", sdists = {"deadbeef": "bar"}, - sha256s_by_version = {"fizz": "bar"}, + hashes_by_version = {"fizz": "bar"}, whls = {"deadb33f": "bar"}, ), "baz": struct( index_url = "https://main.com/baz/", sdists = {"deadbeef": "baz"}, - sha256s_by_version = {"fizz": "baz"}, + hashes_by_version = {"fizz": "baz"}, whls = {"deadb33f": "baz"}, ), "foo": struct( index_url = "https://extra.com/foo/", sdists = {"deadbeef": "foo"}, - sha256s_by_version = {"fizz": "foo"}, + hashes_by_version = {"fizz": "foo"}, whls = {"deadb33f": "foo"}, ), }) @@ -115,7 +115,7 @@ def _test_index_overrides(env): output = struct( sdists = {"deadbeef": url.strip("/").split("/")[-1]}, whls = {"deadb33f": url.strip("/").split("/")[-1]}, - sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + hashes_by_version = {"fizz": url.strip("/").split("/")[-1]}, ), success = True, ) @@ -147,19 +147,19 @@ def _test_index_overrides(env): "ba_z": struct( index_url = "https://main.com/ba-z/", sdists = {"deadbeef": "ba-z"}, - sha256s_by_version = {"fizz": "ba-z"}, + hashes_by_version = {"fizz": "ba-z"}, whls = {"deadb33f": "ba-z"}, ), "bar": struct( index_url = "https://main.com/bar/", sdists = {"deadbeef": "bar"}, - sha256s_by_version = {"fizz": "bar"}, + hashes_by_version = {"fizz": "bar"}, whls = {"deadb33f": "bar"}, ), "foo": struct( index_url = "https://extra.com/foo/", sdists = {"deadbeef": "foo"}, - sha256s_by_version = {"fizz": "foo"}, + hashes_by_version = {"fizz": "foo"}, whls = {"deadb33f": "foo"}, ), }) diff --git a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl index 35e6bcdf9f..968bdc3d2a 100644 --- a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl +++ b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl @@ -25,6 +25,18 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_simple_canonical_digest(env): + got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "sha256:deadbeef") + env.expect.that_str(got).equals("foo_py3_none_any_deadbeef") + +_tests.append(_test_simple_canonical_digest) + +def _test_simple_canonical_digest_other_algo(env): + got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "sha512:deadbeef000deadbeef") + env.expect.that_str(got).equals("foo_py3_none_any_deadbeef") + +_tests.append(_test_simple_canonical_digest_other_algo) + def _test_simple_no_sha(env): got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "") env.expect.that_str(got).equals("foo_1_2_3_py3_none_any") From 3e3e015382b909cf08440c6b8b8f5817b243b86b Mon Sep 17 00:00:00 2001 From: Lequn Chen Date: Thu, 30 Jul 2026 22:26:54 +0000 Subject: [PATCH 3/3] test(pypi): regenerate bzlmod_lockfile facts for the v2 format The committed lock stored pip facts in the v1 format (bare sha256 hex values). The facts are now stored as canonical : values under fact version v2, so the lock check aborted analysis. Co-Authored-By: Claude Fable 5 --- .../bzlmod_lockfile/MODULE.bazel.lock | 646 +++++++++--------- 1 file changed, 323 insertions(+), 323 deletions(-) diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock index 5fac194583..686ee0ee40 100644 --- a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock @@ -286,394 +286,394 @@ "dist_hashes": { "https://pypi.org/simple": { "backports-tarfile": { - "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", - "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz": "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl": "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" }, "certifi": { - "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", - "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz": "741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" + "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl": "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", + "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz": "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" }, "cffi": { - "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl": "89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", - "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl": "8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", - "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl": "1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", - "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl": "f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", - "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", - "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl": "7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", - "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", - "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", - "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl": "df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", - "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl": "1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", - "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", - "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl": "7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", - "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", - "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl": "6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", - "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl": "2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", - "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl": "0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", - "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl": "7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", - "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", - "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl": "bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", - "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl": "961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", - "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", - "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", - "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", - "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl": "c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", - "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl": "1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", - "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl": "19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", - "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl": "78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", - "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", - "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl": "6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", - "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl": "2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", - "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz": "efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", - "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl": "d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", - "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl": "a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", - "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl": "af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", - "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl": "8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", - "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", - "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl": "2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", - "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl": "8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", - "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", - "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", - "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl": "cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", - "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl": "164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", - "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", - "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl": "98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", - "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", - "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", - "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl": "9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", - "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl": "379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", - "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl": "702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", - "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl": "716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", - "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", - "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", - "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl": "fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", - "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", - "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl": "15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", - "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl": "df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", - "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl": "10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", - "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl": "37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", - "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl": "35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", - "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl": "cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", - "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", - "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl": "9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", - "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl": "47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", - "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl": "c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", - "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl": "bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", - "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl": "9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", - "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl": "dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", - "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl": "ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", - "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", - "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl": "0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", - "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl": "86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", - "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl": "a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", - "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl": "b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", - "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", - "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl": "db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", - "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", - "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", - "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl": "3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", - "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl": "f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", - "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl": "c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", - "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl": "02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", - "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl": "510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", - "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl": "c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", - "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl": "d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", - "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl": "7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", - "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl": "6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", - "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", - "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl": "cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", - "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", - "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl": "f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", - "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", - "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl": "64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", - "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", - "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", - "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl": "95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", - "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", - "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl": "4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", - "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", - "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl": "11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", - "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565" + "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl": "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", + "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl": "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", + "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl": "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", + "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl": "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", + "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", + "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl": "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", + "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", + "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", + "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl": "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", + "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl": "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", + "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", + "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl": "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", + "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", + "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", + "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", + "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl": "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", + "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl": "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", + "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", + "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl": "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", + "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl": "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", + "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", + "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", + "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", + "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl": "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", + "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl": "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", + "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", + "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl": "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", + "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", + "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl": "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", + "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl": "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", + "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz": "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", + "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", + "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl": "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", + "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl": "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", + "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl": "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", + "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", + "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl": "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", + "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl": "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", + "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", + "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", + "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl": "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", + "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl": "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", + "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", + "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl": "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", + "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", + "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", + "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl": "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", + "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl": "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", + "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl": "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", + "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl": "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", + "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", + "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", + "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl": "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", + "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", + "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl": "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", + "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl": "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", + "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl": "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", + "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl": "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", + "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl": "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", + "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl": "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", + "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", + "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl": "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", + "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", + "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl": "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", + "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl": "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", + "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl": "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", + "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl": "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", + "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl": "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", + "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", + "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl": "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", + "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl": "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", + "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", + "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl": "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", + "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", + "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl": "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", + "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", + "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", + "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl": "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", + "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl": "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", + "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl": "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", + "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl": "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", + "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl": "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", + "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl": "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", + "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl": "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", + "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl": "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", + "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl": "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", + "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", + "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl": "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", + "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", + "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl": "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", + "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", + "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl": "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", + "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", + "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", + "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl": "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", + "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", + "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl": "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", + "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", + "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl": "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", + "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565" }, "charset-normalizer": { - "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl": "609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", - "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", - "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl": "375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", - "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl": "0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", - "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", - "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl": "65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", - "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl": "78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", - "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", - "https://files.pythonhosted.org/packages/14/cb/1db8b96547ee3186cd2dd7f2e59dd560a9b80748f3604171f3c153d62811/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94", - "https://files.pythonhosted.org/packages/17/6d/bff78a4bacc4891bc63ec5bdc6776d8c85e47fab93d0d5f6223068fad0a4/charset_normalizer-3.4.9-cp39-cp39-win32.whl": "93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9", - "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl": "f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", - "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl": "e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", - "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl": "40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", - "https://files.pythonhosted.org/packages/28/e9/9fb6099b868c82a40698a748ae0fbd4f31ccc13844c176a07158ba2abbfd/charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl": "476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe", - "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", - "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl": "60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", - "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl": "bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", - "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl": "8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", - "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", - "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl": "19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", - "https://files.pythonhosted.org/packages/3d/ca/ad1d7c7d3077dab873f539d3e1d083c0845a762cb0bafdfbe3ef93add598/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f", - "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", - "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl": "a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", - "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl": "c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", - "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl": "416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", - "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", - "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl": "16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", - "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", - "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", - "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl": "1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", - "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", - "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl": "f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", - "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", - "https://files.pythonhosted.org/packages/5f/c0/6eec7bdabe6cbbcc274ec04596f6d93865751a0541d33d60d1ce179bd372/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl": "ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba", - "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl": "79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", - "https://files.pythonhosted.org/packages/6a/05/c94d5cd23396289c54c93b02e0273b4dd8921641d9968c4828caf9bbaad9/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5", - "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl": "69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", - "https://files.pythonhosted.org/packages/6d/46/79847edd07244a4a2d443c6655a7b6ee94203c21539414b059f32713c357/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84", - "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl": "611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", - "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl": "45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", - "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl": "cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", - "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl": "0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", - "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", - "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", - "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl": "9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", - "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl": "4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", - "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl": "ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", - "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl": "03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", - "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl": "75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", - "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl": "90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", - "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", - "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", - "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl": "68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", - "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl": "c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", - "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl": "a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", - "https://files.pythonhosted.org/packages/a2/55/86048bde1c9d0352940bd7b87d825091a52aef67d01cde6c6f7342c5b552/charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl": "ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b", - "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", - "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl": "3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", - "https://files.pythonhosted.org/packages/a6/ec/81e22253f4b7091eca6515bb3da5e45d05a663f7f567bb745695dc60f892/charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl": "253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a", - "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl": "cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", - "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl": "280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", - "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl": "5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", - "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl": "0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", - "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl": "440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", - "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", - "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz": "673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", - "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", - "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl": "432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", - "https://files.pythonhosted.org/packages/c8/53/a8c042eb9eee4716f4d42a0f5a571eb32a09ec429be9fb0b8b9d765393ba/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4", - "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", - "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", - "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", - "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl": "51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", - "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", - "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", - "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl": "67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", - "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", - "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl": "83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", - "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", - "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl": "fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", - "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", - "https://files.pythonhosted.org/packages/eb/78/59344ff9a4a7b5f6530bf7bec2c980047cc42c3a616596cdbd8cb5c1a1af/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl": "43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29", - "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", - "https://files.pythonhosted.org/packages/ec/b4/ef5a49b2e77c00deb43bb3256592b115ba9e4346016e82c516b8d215bf68/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl": "231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4", - "https://files.pythonhosted.org/packages/ed/61/710738687f90d01c06a04ed52d6ca1e62dd9b1d8cc2567098167c4691034/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl": "0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833", - "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl": "6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", - "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", - "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl": "78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", - "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", - "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl": "d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", - "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", - "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl": "898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", - "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl": "ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534" + "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl": "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", + "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", + "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl": "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", + "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl": "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", + "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", + "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl": "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", + "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl": "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", + "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", + "https://files.pythonhosted.org/packages/14/cb/1db8b96547ee3186cd2dd7f2e59dd560a9b80748f3604171f3c153d62811/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94", + "https://files.pythonhosted.org/packages/17/6d/bff78a4bacc4891bc63ec5bdc6776d8c85e47fab93d0d5f6223068fad0a4/charset_normalizer-3.4.9-cp39-cp39-win32.whl": "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9", + "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl": "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", + "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl": "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", + "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl": "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", + "https://files.pythonhosted.org/packages/28/e9/9fb6099b868c82a40698a748ae0fbd4f31ccc13844c176a07158ba2abbfd/charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl": "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe", + "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", + "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl": "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", + "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl": "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", + "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl": "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", + "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", + "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl": "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", + "https://files.pythonhosted.org/packages/3d/ca/ad1d7c7d3077dab873f539d3e1d083c0845a762cb0bafdfbe3ef93add598/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f", + "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", + "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl": "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", + "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl": "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", + "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl": "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", + "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", + "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl": "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", + "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", + "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", + "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl": "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", + "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", + "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl": "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", + "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", + "https://files.pythonhosted.org/packages/5f/c0/6eec7bdabe6cbbcc274ec04596f6d93865751a0541d33d60d1ce179bd372/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl": "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba", + "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl": "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", + "https://files.pythonhosted.org/packages/6a/05/c94d5cd23396289c54c93b02e0273b4dd8921641d9968c4828caf9bbaad9/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5", + "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl": "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", + "https://files.pythonhosted.org/packages/6d/46/79847edd07244a4a2d443c6655a7b6ee94203c21539414b059f32713c357/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84", + "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl": "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", + "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl": "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", + "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl": "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", + "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl": "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", + "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", + "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", + "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl": "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", + "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl": "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", + "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl": "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", + "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl": "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", + "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl": "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", + "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl": "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", + "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", + "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", + "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl": "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", + "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl": "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", + "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl": "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", + "https://files.pythonhosted.org/packages/a2/55/86048bde1c9d0352940bd7b87d825091a52aef67d01cde6c6f7342c5b552/charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl": "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b", + "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", + "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl": "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", + "https://files.pythonhosted.org/packages/a6/ec/81e22253f4b7091eca6515bb3da5e45d05a663f7f567bb745695dc60f892/charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl": "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a", + "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl": "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", + "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl": "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", + "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl": "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", + "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl": "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", + "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl": "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", + "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", + "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz": "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", + "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", + "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl": "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", + "https://files.pythonhosted.org/packages/c8/53/a8c042eb9eee4716f4d42a0f5a571eb32a09ec429be9fb0b8b9d765393ba/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4", + "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", + "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", + "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", + "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl": "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", + "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", + "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", + "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl": "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", + "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", + "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", + "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", + "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl": "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", + "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", + "https://files.pythonhosted.org/packages/eb/78/59344ff9a4a7b5f6530bf7bec2c980047cc42c3a616596cdbd8cb5c1a1af/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl": "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29", + "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", + "https://files.pythonhosted.org/packages/ec/b4/ef5a49b2e77c00deb43bb3256592b115ba9e4346016e82c516b8d215bf68/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl": "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4", + "https://files.pythonhosted.org/packages/ed/61/710738687f90d01c06a04ed52d6ca1e62dd9b1d8cc2567098167c4691034/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl": "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833", + "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl": "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", + "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", + "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl": "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", + "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", + "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl": "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", + "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", + "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl": "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", + "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534" }, "cryptography": { - "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", - "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl": "d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", - "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl": "07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", - "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl": "ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", - "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl": "e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", - "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz": "f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", - "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl": "2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", - "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl": "9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", - "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", - "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", - "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl": "f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", - "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl": "53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", - "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl": "33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", - "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl": "b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", - "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl": "c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", - "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl": "2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", - "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl": "fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", - "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl": "6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", - "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", - "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl": "8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", - "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl": "67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", - "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl": "ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", - "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl": "66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", - "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", - "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl": "42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", - "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl": "b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", - "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl": "966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", - "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl": "2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", - "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl": "0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", - "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl": "0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", - "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl": "b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", - "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl": "4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", - "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl": "cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", - "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl": "be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", - "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl": "084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", - "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl": "6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", - "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", - "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl": "026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", - "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl": "d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", - "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl": "c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", - "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl": "b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", - "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", - "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", - "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl": "b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", - "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl": "73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", - "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl": "28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc" + "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", + "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl": "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", + "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl": "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", + "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl": "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", + "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl": "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", + "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz": "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", + "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl": "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", + "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl": "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", + "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", + "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", + "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl": "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", + "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl": "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", + "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl": "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", + "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl": "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", + "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl": "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", + "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl": "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", + "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl": "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", + "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl": "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", + "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", + "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl": "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", + "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl": "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", + "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl": "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", + "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl": "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", + "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", + "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl": "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", + "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl": "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", + "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl": "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", + "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl": "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", + "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl": "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", + "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl": "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", + "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl": "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", + "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl": "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", + "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl": "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", + "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl": "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", + "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl": "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", + "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl": "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", + "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", + "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl": "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", + "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl": "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", + "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl": "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", + "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl": "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", + "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", + "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", + "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl": "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", + "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl": "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", + "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl": "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc" }, "docutils": { - "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl": "25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", - "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz": "746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e" + "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl": "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", + "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz": "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e" }, "id": { - "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl": "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", - "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz": "d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" + "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl": "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", + "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz": "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" }, "idna": { - "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", - "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz": "ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" + "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl": "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", + "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz": "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" }, "importlib-metadata": { - "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl": "2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", - "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz": "a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc" + "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl": "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", + "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz": "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc" }, "jaraco-classes": { - "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz": "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl": "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" }, "jaraco-context": { - "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz": "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", - "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl": "bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" + "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz": "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", + "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl": "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" }, "jaraco-functools": { - "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl": "99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", - "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz": "880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" + "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl": "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", + "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz": "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" }, "jeepney": { - "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz": "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", - "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl": "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz": "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", + "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl": "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" }, "keyring": { - "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz": "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", - "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl": "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" + "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz": "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", + "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl": "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" }, "markdown-it-py": { - "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz": "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", - "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" + "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz": "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", + "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl": "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" }, "mdurl": { - "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl": "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz": "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" }, "more-itertools": { - "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz": "48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", - "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl": "4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192" + "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz": "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", + "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl": "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192" }, "nh3": { - "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl": "edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", - "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl": "455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", - "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", - "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl": "f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", - "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl": "36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", - "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl": "082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", - "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl": "44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", - "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl": "2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", - "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl": "4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", - "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl": "25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", - "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", - "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl": "889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", - "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl": "f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", - "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl": "34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", - "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", - "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz": "f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", - "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl": "69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", - "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl": "43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", - "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl": "82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", - "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl": "69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", - "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl": "f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", - "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", - "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl": "d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", - "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", - "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl": "5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", - "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl": "597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", - "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25" + "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl": "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", + "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl": "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", + "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", + "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl": "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", + "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl": "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", + "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl": "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", + "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl": "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", + "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl": "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", + "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl": "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", + "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl": "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", + "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", + "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl": "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", + "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", + "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl": "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", + "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", + "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz": "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", + "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl": "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", + "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", + "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl": "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", + "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl": "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", + "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl": "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", + "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", + "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl": "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", + "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", + "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl": "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", + "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl": "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", + "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25" }, "packaging": { - "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz": "ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", - "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl": "5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" + "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz": "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", + "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl": "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" }, "pycparser": { - "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", - "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz": "600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" + "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl": "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", + "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz": "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" }, "pygments": { - "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz": "6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", - "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" + "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz": "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", + "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl": "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" }, "pywin32-ctypes": { - "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", - "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz": "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl": "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" }, "readme-renderer": { - "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz": "030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", - "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl": "3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f" + "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz": "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", + "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl": "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f" }, "requests": { - "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl": "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", - "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz": "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" + "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl": "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz": "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" }, "requests-toolbelt": { - "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", - "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl": "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz": "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" }, "rfc3986": { - "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", - "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz": "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl": "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" }, "rich": { - "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl": "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", - "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz": "edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" + "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl": "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", + "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz": "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" }, "secretstorage": { - "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz": "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", - "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl": "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" + "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz": "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", + "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl": "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" }, "six": { - "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz": "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", - "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl": "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" + "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz": "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", + "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl": "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" }, "twine": { - "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz": "85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", - "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl": "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" + "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz": "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", + "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl": "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" }, "urllib3": { - "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz": "231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", - "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl": "9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" + "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz": "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", + "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl": "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, "zipp": { - "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl": "25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", - "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz": "4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602" + "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl": "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", + "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz": "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602" } } }, - "fact_version": "v1" + "fact_version": "v2" } } }