From 13c34d5466c3db4b08c70cb01f3d390aef8e2755 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 6 Nov 2025 16:40:50 +0100 Subject: [PATCH 1/5] Enforce one platform policy for GardenLinux canonical names Signed-off-by: Tobias Wolf On-behalf-of: SAP --- src/gardenlinux/features/__main__.py | 16 ++++---- src/gardenlinux/features/cname.py | 51 +++++++++++------------ src/gardenlinux/s3/__main__.py | 4 +- src/gardenlinux/s3/s3_artifacts.py | 61 +++++++++------------------- 4 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/gardenlinux/features/__main__.py b/src/gardenlinux/features/__main__.py index 45d4bde..37bcacb 100644 --- a/src/gardenlinux/features/__main__.py +++ b/src/gardenlinux/features/__main__.py @@ -23,7 +23,7 @@ "container_tag", "commit_id", "features", - "platforms", + "platform", "flags", "flavor", "elements", @@ -147,7 +147,7 @@ def main() -> None: "flags", "flavor", "graph", - "platforms", + "platform", ): if args.type == "graph" or len(args.ignore) > 0: features_parser = Parser(gardenlinux_root, feature_dir_name) @@ -270,13 +270,13 @@ def print_output_from_features_parser( flavor, additional_filter_func=additional_filter_func ) ) - elif (output_type in "platforms", "elements", "flags"): + elif (output_type in "platform", "elements", "flags"): features_by_type = parser.filter_as_dict( flavor, additional_filter_func=additional_filter_func ) - if output_type == "platforms": - print(",".join(features_by_type["platform"])) + if output_type == "platform": + print(features_by_type["platform"][0]) elif output_type == "elements": print(",".join(features_by_type["element"])) elif output_type == "flags": @@ -305,8 +305,8 @@ def print_output_from_features_parser( print(cname) elif output_type == "container_name": print(RE_CAMEL_CASE_SPLITTER.sub("\\1_\\2", cname_base).lower()) - elif output_type == "platforms": - print(",".join(features_by_type["platform"])) + elif output_type == "platform": + print(features_by_type["platform"][0]) elif output_type == "elements": print(",".join(features_by_type["element"])) elif output_type == "flags": @@ -331,7 +331,7 @@ def print_output_from_cname(output_type: str, cname_instance: CName) -> None: print(cname_instance.cname) elif output_type == "container_name": print(RE_CAMEL_CASE_SPLITTER.sub("\\1-\\2", cname_instance.flavor).lower()) - elif output_type == "platforms": + elif output_type == "platform": print(cname_instance.feature_set_platform) elif output_type == "elements": print(cname_instance.feature_set_element) diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index d8ce7d7..fa90483 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -6,7 +6,7 @@ import re from configparser import UNNAMED_SECTION, ConfigParser -from os import PathLike +from os import environ, PathLike from pathlib import Path from typing import List, Optional @@ -51,8 +51,9 @@ def __init__(self, cname, arch=None, commit_hash=None, version=None): self._commit_id = None self._feature_elements_cached = None self._feature_flags_cached = None - self._feature_platforms_cached = None + self._feature_platform_cached = None self._feature_set_cached = None + self._flag_multiple_platforms = bool(environ.get("GL_ALLOW_FRANKENSTEIN", False)) self._flavor = None self._version = None @@ -117,7 +118,7 @@ def cname(self) -> str: :return: (str) CName :since: 0.7.0 """ - assert self._flavor is not None, "CName flavor is not set!" + cname = self._flavor if self._arch is not None: @@ -229,14 +230,20 @@ def feature_set_platform(self) -> str: """ Returns the feature set of type "platform" for the cname parsed. - :return: (str) Feature set platforms + :return: (str) Feature set platform :since: 0.11.0 """ - if self._feature_platforms_cached is not None: - return ",".join(self._feature_platforms_cached) + if self._feature_platform_cached is not None: + return self._feature_platform_cached + + platforms = Parser().filter_as_dict(self.flavor)["platform"] + + if self._flag_multiple_platforms: + return ",".join(platforms) - return ",".join(Parser().filter_as_dict(self.flavor)["platform"]) + assert len(platforms) < 2; "Only one platform is supported" + return platforms[0] @property def release_metadata_string(self) -> str: @@ -249,9 +256,12 @@ def release_metadata_string(self) -> str: features = Parser().filter_as_dict(self.flavor) + if not self._flag_multiple_platforms: + assert len(features["platform"]) < 2; "Only one platform is supported" + elements = ",".join(features["element"]) flags = ",".join(features["flag"]) - platforms = ",".join(features["platform"]) + platform = ",".join(features["platform"]) metadata = f""" ID={GL_RELEASE_ID} @@ -264,7 +274,7 @@ def release_metadata_string(self) -> str: BUG_REPORT_URL="{GL_BUG_REPORT_URL}" GARDENLINUX_CNAME="{self.cname}" GARDENLINUX_FEATURES="{self.feature_set}" -GARDENLINUX_FEATURES_PLATFORMS="{platforms}" +GARDENLINUX_FEATURES_PLATFORM="{platform}" GARDENLINUX_FEATURES_ELEMENTS="{elements}" GARDENLINUX_FEATURES_FLAGS="{flags}" GARDENLINUX_VERSION="{self.version}" @@ -282,24 +292,9 @@ def platform(self) -> str: :return: (str) Feature set platforms :since: 0.7.0 """ - assert self._flavor is not None, "Flavor not set!" return self.feature_set_platform - @property - def platforms(self) -> List[str]: - """ - Returns the platforms for the cname parsed. - - :return: (str) Platforms - :since: 0.11.0 - """ - - if self._feature_platforms_cached is not None: - return self._feature_platforms_cached - - return Parser().filter_as_dict(self.flavor)["platform"] - @property def version(self) -> Optional[str]: """ @@ -350,7 +345,7 @@ def load_from_release_file(self, release_file: PathLike | str) -> None: "GARDENLINUX_FEATURES", "GARDENLINUX_FEATURES_ELEMENTS", "GARDENLINUX_FEATURES_FLAGS", - "GARDENLINUX_FEATURES_PLATFORMS", + "GARDENLINUX_FEATURES_PLATFORM", "GARDENLINUX_VERSION", ): if not release_config.has_option(UNNAMED_SECTION, release_field): @@ -395,9 +390,9 @@ def load_from_release_file(self, release_file: PathLike | str) -> None: UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS" ).split(",") - self._feature_platforms_cached = release_config.get( - UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS" - ).split(",") + self._feature_platform_cached = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORM" + ) def save_to_release_file( self, release_file: PathLike | str, overwrite: Optional[bool] = False diff --git a/src/gardenlinux/s3/__main__.py b/src/gardenlinux/s3/__main__.py index eed5140..3d82b97 100644 --- a/src/gardenlinux/s3/__main__.py +++ b/src/gardenlinux/s3/__main__.py @@ -36,4 +36,6 @@ def main() -> None: if args.action == "download-artifacts-from-bucket": S3Artifacts(args.bucket).download_to_directory(args.cname, args.path) elif args.action == "upload-artifacts-to-bucket": - S3Artifacts(args.bucket).upload_from_directory(args.cname, args.path, dry_run=args.dry_run) + S3Artifacts(args.bucket).upload_from_directory( + args.cname, args.path, dry_run=args.dry_run + ) diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index 5d26fa4..a2f0619 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -19,6 +19,7 @@ import yaml from .bucket import Bucket +from ..features import CName class S3Artifacts(object): @@ -116,36 +117,20 @@ def upload_from_directory( artifacts_dir = Path(artifacts_dir) + cname_object = CName(cname) + if not artifacts_dir.is_dir(): raise RuntimeError(f"Artifacts directory given is invalid: {artifacts_dir}") release_file = artifacts_dir.joinpath(f"{cname}.release") release_timestamp = stat(release_file).st_ctime - release_config = ConfigParser(allow_unnamed_section=True) - release_config.read(release_file) - - # Get architecture from the GARDENLINUX_CNAME (second to last element when split by -) - release_cname = release_config.get(UNNAMED_SECTION, "GARDENLINUX_CNAME") - cname_parts = release_cname.split("-") - if len(cname_parts) < 2: - raise RuntimeError(f"Invalid GARDENLINUX_CNAME format in release file: {release_cname}") - arch = cname_parts[-2] # Second to last element is the architecture + cname_object.load_from_release_file(release_file) - commit_id = release_config.get(UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID") - expected_cname = f"{release_cname}-{commit_id}" - - # Verify the provided cname matches the release file - if cname != expected_cname: - raise RuntimeError( - f"Release file cname does not match provided cname: {expected_cname} != {cname}" - ) - - commit_hash = release_config.get(UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG") - - feature_set = release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES") - feature_list = feature_set.split(",") + if cname_object.arch is None: + raise RuntimeError("Architecture could not be determined from cname") + feature_list = cname_object.feature_set requirements_file = artifacts_dir.joinpath(f"{cname}.requirements") require_uefi = None secureboot = None @@ -168,33 +153,25 @@ def upload_from_directory( if secureboot is None: secureboot = "_trustedboot" in feature_list - version = release_config.get(UNNAMED_SECTION, "GARDENLINUX_VERSION") - platform = release_config.get(UNNAMED_SECTION, "GARDENLINUX_PLATFORM") - metadata = { - "platform": platform, - "architecture": arch, + "platform": cname_object.feature_set_platform, + "architecture": cname_object.arch, "base_image": None, - "build_committish": commit_hash, + "build_committish": cname_object.commit_hash, "build_timestamp": datetime.fromtimestamp(release_timestamp).isoformat(), - "gardenlinux_epoch": int(version.split(".", 1)[0]), + "gardenlinux_epoch": int(cname_object.version.split(".", 1)[0]), "logs": None, - "modifiers": feature_list, + "modifiers": cname_object.feature_set, "require_uefi": require_uefi, "secureboot": secureboot, "published_image_metadata": None, "s3_bucket": self._bucket.name, "s3_key": f"meta/singles/{cname}", "test_result": None, - "version": version, + "version": cname_object.version, "paths": [], } - if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT"): - metadata["platform_variant"] = release_config.get( - UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT" - ) - re_object = re.compile("[^a-zA-Z0-9\\s+\\-=.\\_:/@]") for artifact in artifacts_dir.iterdir(): @@ -224,16 +201,14 @@ def upload_from_directory( } s3_tags = { - "architecture": re_object.sub("+", arch), - "platform": re_object.sub("+", platform), - "version": re_object.sub("+", version), - "committish": commit_hash, + "architecture": re_object.sub("+", cname_object.arch), + "platform": re_object.sub("+", cname_object.platform), + "version": re_object.sub("+", cname_object.version), + "committish": cname_object.commit_hash, "md5sum": md5sum, "sha256sum": sha256sum, } - metadata["paths"].append(artifact_metadata) - if not dry_run: if delete_before_push: self._bucket.delete_objects(Delete={"Objects": [{"Key": s3_key}]}) @@ -244,6 +219,8 @@ def upload_from_directory( ExtraArgs={"Tagging": urlencode(s3_tags)}, ) + metadata["paths"].append(artifact_metadata) + if dry_run: print(yaml.dump(metadata, sort_keys=False)) else: From 45daef11f641e8abc41a6350ca340699d2080953 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 6 Nov 2025 16:41:31 +0100 Subject: [PATCH 2/5] Adapt unit tests for GardenLinux feature changes Signed-off-by: Tobias Wolf On-behalf-of: SAP --- src/gardenlinux/features/__main__.py | 53 +++----- src/gardenlinux/features/cname.py | 12 +- src/gardenlinux/features/cname_main.py | 6 +- src/gardenlinux/features/parser.py | 71 ++++++++--- test-data/gardenlinux | 2 +- tests/features/test_cname_main.py | 7 +- tests/features/test_features_parser.py | 71 ----------- tests/features/test_main.py | 124 ++++-------------- tests/features/test_metadata_main.py | 2 +- tests/features/test_parser.py | 166 +++++++++++++++++++++++++ tests/s3/conftest.py | 2 +- tests/s3/test_main.py | 12 +- tests/s3/test_s3_artifacts.py | 65 ++-------- 13 files changed, 302 insertions(+), 291 deletions(-) delete mode 100644 tests/features/test_features_parser.py create mode 100644 tests/features/test_parser.py diff --git a/src/gardenlinux/features/__main__.py b/src/gardenlinux/features/__main__.py index 37bcacb..9fbb162 100644 --- a/src/gardenlinux/features/__main__.py +++ b/src/gardenlinux/features/__main__.py @@ -9,7 +9,6 @@ import logging import os import re -from functools import reduce from os import path from typing import Any, List, Set @@ -167,21 +166,6 @@ def main() -> None: print(f"{version}-{commit_id_or_hash[:8]}") -def get_flavor(sorted_features: List[str]): - """ - Get the base cname for the feature set given. - - :param sorted_features: Sorted feature set - - :return: (str) Base cname - :since: 0.7.0 - """ - - return reduce( - lambda a, b: a + ("-" if not b.startswith("_") else "") + b, sorted_features - ) - - def get_version_and_commit_id_from_files(gardenlinux_root: str) -> tuple[str, str]: """ Returns the version and commit ID based on files in the GardenLinux root directory. @@ -287,9 +271,9 @@ def print_output_from_features_parser( sorted_features = Parser.sort_graph_nodes(graph) minimal_feature_set = get_minimal_feature_set(graph) - sorted_minimal_features = sort_subset(minimal_feature_set, sorted_features) + sorted_minimal_features = Parser.subset(minimal_feature_set, sorted_features) - cname_base = get_flavor(sorted_minimal_features) + cname_base = Parser.get_flavor_from_feature_set(sorted_minimal_features) if output_type == "cname_base": print(cname_base) @@ -325,10 +309,21 @@ def print_output_from_cname(output_type: str, cname_instance: CName) -> None: :since: 0.11.0 """ - if output_type == "cname_base": - print(cname_instance.flavor) - elif output_type == "cname": - print(cname_instance.cname) + if output_type in ("cname_base", "cname", "flavor"): + sorted_features = Parser.get_flavor_as_feature_set(cname_instance.flavor) + flavor = Parser.get_flavor_from_feature_set(sorted_features) + + if output_type in ("cname_base", "flavor"): + print(flavor) + else: + if cname_instance.version_and_commit_id is None: + raise RuntimeError( + "Version and commit ID can't be provided without appropriate input." + ) + + print( + f"{flavor}-{cname_instance.arch}-{cname_instance.version_and_commit_id}" + ) elif output_type == "container_name": print(RE_CAMEL_CASE_SPLITTER.sub("\\1-\\2", cname_instance.flavor).lower()) elif output_type == "platform": @@ -341,19 +336,5 @@ def print_output_from_cname(output_type: str, cname_instance: CName) -> None: print(cname_instance.feature_set_flag) -def sort_subset(input_set: Set[str], order_list: List[str]) -> List[str]: - """ - Returns items from `order_list` if given in `input_set`. - - :param input_set: Set of values - :param order_list: networkx.Digraph - - :return: (str) mermaid.js representation - :since: 0.7.0 - """ - - return [item for item in order_list if item in input_set] - - if __name__ == "__main__": main() diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index fa90483..210f2f0 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -53,7 +53,11 @@ def __init__(self, cname, arch=None, commit_hash=None, version=None): self._feature_flags_cached = None self._feature_platform_cached = None self._feature_set_cached = None - self._flag_multiple_platforms = bool(environ.get("GL_ALLOW_FRANKENSTEIN", False)) + + self._flag_multiple_platforms = bool( + environ.get("GL_ALLOW_FRANKENSTEIN", False) + ) + self._flavor = None self._version = None @@ -242,7 +246,8 @@ def feature_set_platform(self) -> str: if self._flag_multiple_platforms: return ",".join(platforms) - assert len(platforms) < 2; "Only one platform is supported" + assert len(platforms) < 2 + "Only one platform is supported" return platforms[0] @property @@ -257,7 +262,8 @@ def release_metadata_string(self) -> str: features = Parser().filter_as_dict(self.flavor) if not self._flag_multiple_platforms: - assert len(features["platform"]) < 2; "Only one platform is supported" + assert len(features["platform"]) < 2 + "Only one platform is supported" elements = ",".join(features["element"]) flags = ",".join(features["flag"]) diff --git a/src/gardenlinux/features/cname_main.py b/src/gardenlinux/features/cname_main.py index 9ddc1e3..9f86e75 100644 --- a/src/gardenlinux/features/cname_main.py +++ b/src/gardenlinux/features/cname_main.py @@ -11,10 +11,8 @@ from os.path import basename, dirname from .__main__ import ( - get_flavor, get_minimal_feature_set, get_version_and_commit_id_from_files, - sort_subset, ) from .cname import CName from .parser import Parser @@ -78,9 +76,9 @@ def main(): sorted_features = Parser.sort_graph_nodes(graph) minimal_feature_set = get_minimal_feature_set(graph) - sorted_minimal_features = sort_subset(minimal_feature_set, sorted_features) + sorted_minimal_features = Parser.subset(minimal_feature_set, sorted_features) - generated_cname = get_flavor(sorted_minimal_features) + generated_cname = Parser.get_flavor_from_feature_set(sorted_minimal_features) generated_cname += f"-{cname.arch}" diff --git a/src/gardenlinux/features/parser.py b/src/gardenlinux/features/parser.py index 7f00374..9300f6a 100644 --- a/src/gardenlinux/features/parser.py +++ b/src/gardenlinux/features/parser.py @@ -6,9 +6,10 @@ import logging import os +from functools import reduce from glob import glob from pathlib import Path -from typing import Callable, Optional, cast +from typing import Callable, List, Optional, Set import networkx import yaml @@ -131,7 +132,7 @@ def filter( :since: 0.7.0 """ - feature_set = Parser.get_cname_as_feature_set(cname) + feature_set = Parser.get_flavor_as_feature_set(cname) return self.filter_based_on_feature_set( feature_set, ignore_excludes, additional_filter_func @@ -282,11 +283,11 @@ def filter_based_on_feature_set( self.graph.add_node("libc", content=BARE_FLAVOR_LIBC_FEATURE_CONTENT) for feature in feature_set: - filter_set.update( - networkx.descendants( - Parser._get_graph_view_for_attr(self.graph, "include"), feature - ) - ) + for node in networkx.descendants( + Parser._get_graph_view_for_attr(self.graph, "include"), feature + ): + if node not in filter_set: + filter_set.append(node) graph = networkx.subgraph_view( self.graph, @@ -310,9 +311,7 @@ def _exclude_from_filter_set(graph, feature_set, filter_set): :since: 0.7.0 """ - exclude_graph_view = cast( - networkx.DiGraph, Parser._get_graph_view_for_attr(graph, "exclude") - ) + exclude_graph_view = Parser._get_graph_view_for_attr(graph, "exclude") exclude_list = [] for node in networkx.lexicographical_topological_sort(graph): @@ -362,18 +361,48 @@ def _read_feature_yaml(self, feature_yaml_file: str): return {"name": name, "content": content} @staticmethod - def get_cname_as_feature_set(cname): + def get_flavor_from_feature_set(sorted_features: List[str]): + """ + Get the base cname for the feature set given. + + :param sorted_features: Sorted feature set + + :return: (str) Base cname + :since: 0.7.0 + """ + + return reduce( + lambda a, b: a + ("-" if not b.startswith("_") else "") + b, sorted_features + ) + + @staticmethod + def get_flavor_as_feature_set(cname): """ Returns the features of a given canonical name. :param cname: Canonical name - :return: (set) Features of the cname - :since: 0.7.0 + :return: (list) Features of the cname + :since: 0.11.0 """ cname = cname.replace("_", "-_") - return set(cname.split("-")) + + platform = None + features = [] + flags = [] + + for feature in cname.split("-"): + if platform is None: + platform = feature + continue + + if feature[:1] == "_": + flags.append(feature) + else: + features.append(feature) + + return [platform] + sorted(features) + sorted(flags) @staticmethod def _get_filter_set_callable(filter_set, additional_filter_func): @@ -473,6 +502,20 @@ def key_function(node): return list(networkx.lexicographical_topological_sort(graph, key=key_function)) + @staticmethod + def subset(input_set: Set[str], order_list: List[str]) -> List[str]: + """ + Returns items from `order_list` if given in `input_set`. + + :param input_set: Set of values for filtering + :param order_list: Set of values to be filtered + + :return: (list) Subset + :since: 0.11.0 + """ + + return [item for item in order_list if item in input_set] + @staticmethod def sort_reversed_graph_nodes(graph): """ diff --git a/test-data/gardenlinux b/test-data/gardenlinux index c81fcc9..8c14e75 160000 --- a/test-data/gardenlinux +++ b/test-data/gardenlinux @@ -1 +1 @@ -Subproject commit c81fcc9faebdba3193beeb9e1bfd5b1ac9fbf107 +Subproject commit 8c14e75011397cd6d16c1182f3377678c885b87a diff --git a/tests/features/test_cname_main.py b/tests/features/test_cname_main.py index 6c3151c..2362542 100644 --- a/tests/features/test_cname_main.py +++ b/tests/features/test_cname_main.py @@ -5,6 +5,7 @@ import pytest import gardenlinux.features.cname_main as cname_main +from gardenlinux.features import Parser def test_main_happy(monkeypatch, capsys): @@ -19,7 +20,7 @@ class FakeGraph: in_degree = lambda self: [("f1", 0)] edges = [("f1", "f2")] - class FakeParser: + class FakeParser(Parser): def __init__(self, *a, **k): pass @@ -55,7 +56,7 @@ def test_main_version_from_file(monkeypatch, capsys): lambda root: ("2.0", "abcdef12"), ) - class FakeParser: + class FakeParser(Parser): def __init__(self, *a, **k): pass @@ -96,7 +97,7 @@ def raise_runtime(_): ) # Patch Parser for minimal valid graph - class FakeParser: + class FakeParser(Parser): def __init__(self, *a, **k): pass diff --git a/tests/features/test_features_parser.py b/tests/features/test_features_parser.py deleted file mode 100644 index f4b83b4..0000000 --- a/tests/features/test_features_parser.py +++ /dev/null @@ -1,71 +0,0 @@ -import pytest - -from gardenlinux.features import Parser - -from ..constants import GL_ROOT_DIR - - -@pytest.mark.parametrize( - "input_cname, expected_output", - [ - ( - "aws-gardener_prod", - { - "platform": ["aws"], - "element": ["log", "sap", "ssh", "base", "server", "cloud", "gardener"], - "flag": ["_boot", "_nopkg", "_prod", "_slim"], - }, - ), - ( - "gcp-gardener_prod", - { - "platform": ["gcp"], - "element": ["log", "sap", "ssh", "base", "server", "cloud", "gardener"], - "flag": ["_boot", "_nopkg", "_prod", "_slim"], - }, - ), - ( - "azure-gardener_prod", - { - "platform": ["azure"], - "element": ["log", "sap", "ssh", "base", "server", "cloud", "gardener"], - "flag": ["_boot", "_nopkg", "_prod", "_slim"], - }, - ), - ( - "ali-gardener_prod", - { - "platform": ["ali"], - "element": ["log", "sap", "ssh", "base", "server", "cloud", "gardener"], - "flag": ["_boot", "_nopkg", "_prod", "_slim"], - }, - ), - ( - "metal-khost_dev", - { - "platform": ["metal"], - "element": [ - "firewall", - "log", - "sap", - "ssh", - "base", - "server", - "chost", - "khost", - ], - "flag": ["_boot", "_dev", "_selinux", "_slim"], - }, - ), - ], -) -def test_parser_filter_as_dict(input_cname: str, expected_output: dict): - """ - Tests if parser_filter_as_dict returns the dict with expected features. - - If you discover that this test failed, you may want to verify if the included - features have changed since writing this test. In this case, update the expected output accordingly. - You can print the output of parser_filter_as_dict so you have the dict in the expected format. - """ - features_dict = Parser(GL_ROOT_DIR).filter_as_dict(input_cname) - assert features_dict == expected_output diff --git a/tests/features/test_main.py b/tests/features/test_main.py index 6b302ec..d10d862 100644 --- a/tests/features/test_main.py +++ b/tests/features/test_main.py @@ -13,57 +13,6 @@ # ------------------------------- -def test_get_flavor(): - # Arrange - sorted_features = ["base", "_hidden", "extra"] - - # Act - result = fema.get_flavor(sorted_features) - - # Assert - assert result == "base_hidden-extra" - - -def test_get_flavor_empty_raises(): - # get_flavor with empty iterable raises TypeError - with pytest.raises(TypeError): - fema.get_flavor([]) - - -def test_sort_return_intersection_subset(): - # Arrange - input_set = {"a", "c"} - order_list = ["a", "b", "c", "d"] - - # Act - result = fema.sort_subset(input_set, order_list) - - # Assert - assert result == ["a", "c"] - - -def test_sort_subset_nomatch(): - # Arrange - input_set = {"x", "y"} - order_list = ["a", "b", "c"] - - # Act - result = fema.sort_subset(input_set, order_list) - - # Assert - assert result == [] - - -def test_sort_subset_with_empty_order_list(): - # Arrange - input_set = {"a", "b"} - order_list = [] - - result = fema.sort_subset(input_set, order_list) - - assert result == [] - - def test_graph_mermaid(): # Arrange class FakeGraph: @@ -298,16 +247,31 @@ def test_main_requires_cname(monkeypatch): fema.main() +def test_main_cname_raises_missing_commit_id(monkeypatch): + # Arrange + # args.type == 'cname, arch is None and no default_arch set + argv = [ + "prog", + "--cname", + "flav", + "--default-arch", + "amd64", + "--version", + "1.0", + "cname", + ] + monkeypatch.setattr(sys, "argv", argv) + + # Act / Assert + with pytest.raises(RuntimeError, match="Version and commit ID"): + fema.main() + + def test_main_raises_no_arch_no_default(monkeypatch): # Arrange # args.type == 'cname, arch is None and no default_arch set argv = ["prog", "--cname", "flav", "cname"] monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr( - fema, - "Parser", - lambda *a, **kw: types.SimpleNamespace(filter=lambda *a, **k: None), - ) # Act / Assert with pytest.raises(RuntimeError, match="Architecture could not be determined"): @@ -334,45 +298,6 @@ def test_main_raises_missing_commit_id(monkeypatch, capsys): fema.main() -def test_main_with_cname_print_cname(monkeypatch, capsys): - # Arrange - class FakeGraph: - def in_degree(self): - # Simulate a graph where one feature has no dependencies - return [("f1", 0)] - - class FakeParser: - def __call__(self, *a, **k): - return types.SimpleNamespace(filter=lambda *a, **k: FakeGraph()) - - @staticmethod - def get_cname_as_feature_set(f): - return {"f1"} - - @staticmethod - def sort_graph_nodes(graph): - return ["f1"] - - @staticmethod - def sort_subset(subset, length): - return [] - - monkeypatch.setattr(fema, "Parser", FakeParser()) - - monkeypatch.setattr( - sys, - "argv", - ["prog", "--cname", "flav", "--arch", "amd64", "--version", "1.0", "cname"], - ) - - # Act - fema.main() - - # Assert - captured = capsys.readouterr() - assert "flav" in captured.out - - def test_main_with_exclude_cname_print_elements(monkeypatch, capsys): # Arrange monkeypatch.setattr( @@ -402,10 +327,7 @@ def test_main_with_exclude_cname_print_elements(monkeypatch, capsys): # Assert captured = capsys.readouterr().out.strip() - assert ( - "log,sap,ssh,base,server,gardener" - == captured - ) + assert "log,sap,ssh,base,server,multipath,iscsi,nvme,gardener" == captured def test_main_with_exclude_cname_print_features(monkeypatch, capsys): @@ -420,7 +342,7 @@ def test_main_with_exclude_cname_print_features(monkeypatch, capsys): "--cname", "kvm-gardener_prod", "--ignore", - "cloud", + "log", "--arch", "amd64", "--version", @@ -438,6 +360,6 @@ def test_main_with_exclude_cname_print_features(monkeypatch, capsys): captured = capsys.readouterr().out.strip() assert ( - "log,sap,ssh,_boot,_ignite,kvm,_nopkg,_prod,_slim,base,server,gardener" + "sap,ssh,_fwcfg,_ignite,_legacy,_nopkg,_prod,_slim,base,server,cloud,kvm,multipath,iscsi,nvme,gardener" == captured ) diff --git a/tests/features/test_metadata_main.py b/tests/features/test_metadata_main.py index 0e1d092..ebd731a 100644 --- a/tests/features/test_metadata_main.py +++ b/tests/features/test_metadata_main.py @@ -26,7 +26,7 @@ def get_container_amd64_release_metadata(version, commit_hash): BUG_REPORT_URL="{GL_BUG_REPORT_URL}" GARDENLINUX_CNAME="container-amd64-today-local" GARDENLINUX_FEATURES="_slim,base,container" -GARDENLINUX_FEATURES_PLATFORMS="container" +GARDENLINUX_FEATURES_PLATFORM="container" GARDENLINUX_FEATURES_ELEMENTS="base" GARDENLINUX_FEATURES_FLAGS="_slim" GARDENLINUX_VERSION="today" diff --git a/tests/features/test_parser.py b/tests/features/test_parser.py new file mode 100644 index 0000000..9917ad8 --- /dev/null +++ b/tests/features/test_parser.py @@ -0,0 +1,166 @@ +import pytest + +from gardenlinux.features import Parser + +from ..constants import GL_ROOT_DIR + + +@pytest.mark.parametrize( + "input_cname, expected_output", + [ + ( + "aws-gardener_prod", + { + "platform": ["aws"], + "element": [ + "log", + "sap", + "ssh", + "base", + "server", + "cloud", + "multipath", + "iscsi", + "nvme", + "gardener", + ], + "flag": ["_fwcfg", "_legacy", "_nopkg", "_prod", "_slim"], + }, + ), + ( + "gcp-gardener_prod", + { + "platform": ["gcp"], + "element": [ + "log", + "sap", + "ssh", + "base", + "server", + "cloud", + "multipath", + "iscsi", + "nvme", + "gardener", + ], + "flag": ["_fwcfg", "_legacy", "_nopkg", "_prod", "_slim"], + }, + ), + ( + "azure-gardener_prod", + { + "platform": ["azure"], + "element": [ + "log", + "sap", + "ssh", + "base", + "server", + "cloud", + "multipath", + "iscsi", + "nvme", + "gardener", + ], + "flag": ["_fwcfg", "_legacy", "_nopkg", "_prod", "_slim"], + }, + ), + ( + "ali-gardener_prod", + { + "platform": ["ali"], + "element": [ + "log", + "sap", + "ssh", + "base", + "server", + "cloud", + "multipath", + "iscsi", + "nvme", + "gardener", + ], + "flag": ["_fwcfg", "_legacy", "_nopkg", "_prod", "_slim"], + }, + ), + ( + "metal-khost_dev", + { + "platform": ["metal"], + "element": [ + "firewall", + "log", + "sap", + "ssh", + "base", + "server", + "chost", + "khost", + ], + "flag": ["_dev", "_fwcfg", "_legacy", "_selinux", "_slim"], + }, + ), + ], +) +def test_parser_filter_as_dict(input_cname: str, expected_output: dict): + """ + Tests if parser_filter_as_dict returns the dict with expected features. + + If you discover that this test failed, you may want to verify if the included + features have changed since writing this test. In this case, update the expected output accordingly. + You can print the output of parser_filter_as_dict so you have the dict in the expected format. + """ + features_dict = Parser(GL_ROOT_DIR).filter_as_dict(input_cname) + assert features_dict == expected_output + + +def test_parser_return_intersection_subset(): + # Arrange + input_set = {"a", "c"} + order_list = ["a", "b", "c", "d"] + + # Act + result = Parser.subset(input_set, order_list) + + # Assert + assert result == ["a", "c"] + + +def test_get_flavor_from_feature_set(): + # Arrange + sorted_features = ["base", "_hidden", "extra"] + + # Act + result = Parser.get_flavor_from_feature_set(sorted_features) + + # Assert + assert result == "base_hidden-extra" + + +def test_gget_flavor_from_feature_set_empty_raises(): + # get_flavor with empty iterable raises TypeError + with pytest.raises(TypeError): + Parser.get_flavor_from_feature_set([]) + + +def test_parser_subset_nomatch(): + # Arrange + input_set = {"x", "y"} + order_list = ["a", "b", "c"] + + # Act + result = Parser.subset(input_set, order_list) + + # Assert + assert result == [] + + +def test_parser_subset_with_empty_order_list(): + # Arrange + input_set = {"a", "b"} + order_list = [] + + result = Parser.subset(input_set, order_list) + + assert result == [] diff --git a/tests/s3/conftest.py b/tests/s3/conftest.py index 9af13a9..b2a8e64 100644 --- a/tests/s3/conftest.py +++ b/tests/s3/conftest.py @@ -22,7 +22,7 @@ class S3Env: def make_cname( - flavor: str = "kvm-container", + flavor: str = "container", arch: str = "amd64", version: str = "1234.1", commit: str = "abc123", diff --git a/tests/s3/test_main.py b/tests/s3/test_main.py index b9afe12..887de20 100644 --- a/tests/s3/test_main.py +++ b/tests/s3/test_main.py @@ -7,7 +7,7 @@ @pytest.mark.parametrize( - "argv,expected_method", + "argv, expected_method, expected_args, expected_kwargs", [ ( [ @@ -21,6 +21,8 @@ "download-artifacts-from-bucket", ], "download_to_directory", + ["test-cname", "some/path"], + {}, ), ( [ @@ -34,10 +36,14 @@ "upload-artifacts-to-bucket", ], "upload_from_directory", + ["test-cname", "some/path"], + {"dry_run": False}, ), ], ) -def test_main_calls_correct_artifacts(argv, expected_method): +def test_main_calls_correct_artifacts( + argv, expected_method, expected_args, expected_kwargs +): with patch.object(sys, "argv", argv): with patch.object(s3m, "S3Artifacts") as mock_s3_cls: mock_instance = MagicMock() @@ -46,6 +52,6 @@ def test_main_calls_correct_artifacts(argv, expected_method): s3m.main() method = getattr(mock_instance, expected_method) - method.assert_called_once_with("test-cname", "some/path") + method.assert_called_once_with(*expected_args, **expected_kwargs) mock_s3_cls.assert_called_once_with("test-bucket") diff --git a/tests/s3/test_s3_artifacts.py b/tests/s3/test_s3_artifacts.py index 0b1cfda..d15fe6e 100644 --- a/tests/s3/test_s3_artifacts.py +++ b/tests/s3/test_s3_artifacts.py @@ -9,10 +9,14 @@ from gardenlinux.s3.s3_artifacts import S3Artifacts RELEASE_DATA = """ + GARDENLINUX_CNAME = container-amd64-1234.1-abc123 GARDENLINUX_VERSION = 1234.1 GARDENLINUX_COMMIT_ID = abc123 GARDENLINUX_COMMIT_ID_LONG = abc123long GARDENLINUX_FEATURES = _usi,_trustedboot + GARDENLINUX_FEATURES_ELEMENTS = + GARDENLINUX_FEATURES_FLAGS = _usi,_trustedboot + GARDENLINUX_FEATURES_PLATFORM = container """ @@ -101,7 +105,7 @@ def test_upload_from_directory_success(s3_setup): release_path = env.tmp_path / f"{env.cname}.release" release_path.write_text(RELEASE_DATA) - for filename in [f"{env.cname}-file1", f"{env.cname}-file2"]: + for filename in [f"{env.cname}-file1", f"{env.cname}-file2", "container"]: (env.tmp_path / filename).write_bytes(b"dummy content") # Act @@ -124,7 +128,7 @@ def test_upload_from_directory_success(s3_setup): Bucket=env.bucket_name, Key=f"objects/{env.cname}/{env.cname}-file1" ) tags = {tag["Key"]: tag["Value"] for tag in raw_tags_response["TagSet"]} - assert tags["platform"] == "container+kvm" + assert tags["platform"] == "container" def test_upload_from_directory_with_delete(s3_setup): @@ -160,30 +164,6 @@ def test_upload_from_directory_with_delete(s3_setup): assert f"meta/singles/{env.cname}" in keys -def test_upload_from_directory_arch_none_raises(monkeypatch, s3_setup): - """Raise RuntimeError when CName has no arch""" - # Arrange - env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" - release_path.write_text(RELEASE_DATA) - - # Monkeypatch CName to simulate missing architecture - import gardenlinux.s3.s3_artifacts as s3art - - class DummyCName: - arch = None - - def __init__(self, cname): - pass - - monkeypatch.setattr(s3art, "CName", DummyCName) - - # Act / Assert - artifacts = S3Artifacts(env.bucket_name) - with pytest.raises(RuntimeError, match="Architecture could not be determined"): - artifacts.upload_from_directory(env.cname, env.tmp_path) - - def test_upload_from_directory_invalid_dir_raises(s3_setup): """Raise RuntimeError if artifacts_dir is invalid""" env = s3_setup @@ -204,41 +184,20 @@ def test_upload_from_directory_version_mismatch_raises(s3_setup): artifacts = S3Artifacts(env.bucket_name) # Act / Assert - with pytest.raises(RuntimeError, match="Version"): + with pytest.raises(RuntimeError, match="failed consistency check"): artifacts.upload_from_directory(env.cname, env.tmp_path) -def test_upload_from_directory_none_version_raises(monkeypatch, s3_setup): +def test_upload_from_directory_succeeds_because_of_release_file(monkeypatch, s3_setup): """ Raise RuntimeError if CName.version is None. """ # Arrange env = s3_setup - (env.tmp_path / f"{env.cname}.release").write_text(RELEASE_DATA) - - import gardenlinux.s3.s3_artifacts as s3art - - # Monkeypatch CName to force Cname.version to be None and avoid - # class internal error checks - class DummyCName: - arch = "amd64" - version = None - commit_id = "abc123" - platform = "aws" - - def __init__(self, cname): - pass + (env.tmp_path / f"container.release").write_text(RELEASE_DATA) - monkeypatch.setattr(s3art, "CName", DummyCName) - - artifacts = s3art.S3Artifacts(env.bucket_name) - - # Act / Assert - with pytest.raises( - RuntimeError, - match="Release file data and given cname conflict detected: Version None", - ): - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts = S3Artifacts(env.bucket_name) + artifacts.upload_from_directory("container", env.tmp_path) def test_upload_from_directory_invalid_artifact_name(s3_setup): @@ -273,7 +232,7 @@ def test_upload_from_directory_commit_mismatch_raises(s3_setup): artifacts = S3Artifacts(env.bucket_name) # Act / Assert - with pytest.raises(RuntimeError, match="Commit ID"): + with pytest.raises(RuntimeError, match="failed consistency check"): artifacts.upload_from_directory(env.cname, env.tmp_path) From 21e0600b029ac613f348b491af1deb27f16ba36d Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 17 Nov 2025 13:45:07 +0100 Subject: [PATCH 3/5] Fix formatting issues in `gardenlinux.features` and `gardenlinux.s3` Signed-off-by: Tobias Wolf On-behalf-of: SAP --- src/gardenlinux/features/cname.py | 2 +- src/gardenlinux/s3/s3_artifacts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index 210f2f0..2edf4fb 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -6,7 +6,7 @@ import re from configparser import UNNAMED_SECTION, ConfigParser -from os import environ, PathLike +from os import PathLike, environ from pathlib import Path from typing import List, Optional diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index a2f0619..4d63338 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -18,8 +18,8 @@ import yaml -from .bucket import Bucket from ..features import CName +from .bucket import Bucket class S3Artifacts(object): From 7299a8a40255e3d2bce1d49592367203b258f1d3 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 17 Nov 2025 15:52:18 +0100 Subject: [PATCH 4/5] Fix an issue found by `mypy` Signed-off-by: Tobias Wolf On-behalf-of: SAP --- src/gardenlinux/features/__main__.py | 47 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/gardenlinux/features/__main__.py b/src/gardenlinux/features/__main__.py index 9fbb162..c5eb530 100644 --- a/src/gardenlinux/features/__main__.py +++ b/src/gardenlinux/features/__main__.py @@ -10,7 +10,7 @@ import os import re from os import path -from typing import Any, List, Set +from typing import Any, Set from .cname import CName from .parser import Parser @@ -67,12 +67,11 @@ def main() -> None: args = parser.parse_args() - assert bool(args.feature_dir) or bool( - args.release_file - ), "Please provide either `--feature_dir` or `--release_file` argument" + assert bool(args.feature_dir) or bool(args.release_file), ( + "Please provide either `--feature_dir` or `--release_file` argument" + ) arch = args.arch - flavor = None commit_id_or_hash = args.commit gardenlinux_root = path.dirname(args.feature_dir) version = args.version @@ -97,18 +96,15 @@ def main() -> None: version = args.default_version - if args.cname: - cname = CName( - args.cname, arch=arch, commit_hash=commit_id_or_hash, version=version - ) + cname = CName(args.cname, arch=arch, commit_hash=commit_id_or_hash, version=version) - if args.release_file is not None: - cname.load_from_release_file(args.release_file) + if args.release_file is not None: + cname.load_from_release_file(args.release_file) - arch = cname.arch - flavor = cname.flavor - commit_id_or_hash = cname.commit_id - version = cname.version + arch = cname.arch + flavor = cname.flavor + commit_id_or_hash = cname.commit_id + version = cname.version if (arch is None or arch == "") and ( args.type in ("cname", "container_name", "arch") @@ -152,7 +148,7 @@ def main() -> None: features_parser = Parser(gardenlinux_root, feature_dir_name) print_output_from_features_parser( - args.type, features_parser, flavor, args.ignore + args.type, cname, features_parser, flavor, args.ignore ) else: print_output_from_cname(args.type, cname) @@ -233,7 +229,11 @@ def graph_as_mermaid_markup(flavor: str | None, graph: Any) -> str: def print_output_from_features_parser( - output_type: str, parser: Parser, flavor: str, ignores_list: set + output_type: str, + cname_instance: CName, + parser: Parser, + flavor: str, + ignores_list: set, ) -> None: """ Prints output to stdout based on the given features parser and parameters. @@ -246,7 +246,8 @@ def print_output_from_features_parser( :since: 0.11.0 """ - additional_filter_func = lambda node: node not in ignores_list + def additional_filter_func(node): + return node not in ignores_list if output_type == "features": print( @@ -254,7 +255,7 @@ def print_output_from_features_parser( flavor, additional_filter_func=additional_filter_func ) ) - elif (output_type in "platform", "elements", "flags"): + elif output_type in ("platform", "elements", "flags"): features_by_type = parser.filter_as_dict( flavor, additional_filter_func=additional_filter_func ) @@ -280,11 +281,11 @@ def print_output_from_features_parser( elif output_type == "cname": cname = flavor - if arch is not None: - cname += f"-{arch}" + if cname_instance.arch is not None: + cname += f"-{cname_instance.arch}" - if commit_id_or_hash is not None: - cname += f"-{version}-{commit_id_or_hash[:8]}" + if cname_instance.version_and_commit_id is not None: + cname += f"-{cname_instance.version_and_commit_id}" print(cname) elif output_type == "container_name": From e204f5f83ee7163dcc239e39eba20f6d9ef01637 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 17 Nov 2025 15:59:35 +0100 Subject: [PATCH 5/5] Replace `pyright` with `pre-commit` Signed-off-by: Tobias Wolf On-behalf-of: SAP --- .pre-commit-config.ruff.yaml | 7 + .pre-commit-config.yaml | 25 + Makefile | 13 +- docs/index.rst | 1 - hack/print_feature_extensions.sh | 10 +- poetry.lock | 683 +++++++++--------- pyproject.toml | 28 +- pyrightconfig.json | 27 - src/gardenlinux/apt/debsource.py | 6 +- src/gardenlinux/apt/package_repo_info.py | 2 +- src/gardenlinux/features/cname.py | 2 +- src/gardenlinux/features/parser.py | 72 +- src/gardenlinux/flavors/parser.py | 55 +- src/gardenlinux/git/repository.py | 47 +- src/gardenlinux/logger.py | 3 +- src/gardenlinux/oci/container.py | 6 +- src/gardenlinux/oci/index.py | 11 +- src/gardenlinux/oci/layer.py | 33 +- src/gardenlinux/oci/manifest.py | 31 +- src/gardenlinux/oci/platform.py | 3 +- src/gardenlinux/s3/bucket.py | 29 +- src/gardenlinux/s3/s3_artifacts.py | 8 +- tests/apt/test_debsource.py | 2 +- tests/apt/test_package_repo_info.py | 29 +- tests/features/test_cname_main.py | 4 +- tests/features/test_main.py | 2 +- tests/github/test_create_github_release.py | 12 +- .../test_create_github_release_notes.py | 21 +- tests/github/test_github_script.py | 13 +- .../test_upload_to_github_release_page.py | 18 +- tests/helper.py | 7 +- tests/oci/test_index.py | 13 +- tests/oci/test_layer.py | 31 +- tests/oci/test_oci.py | 44 +- tests/s3/conftest.py | 18 +- tests/s3/test_bucket.py | 17 +- tests/s3/test_s3_artifacts.py | 2 +- 37 files changed, 670 insertions(+), 665 deletions(-) create mode 100644 .pre-commit-config.ruff.yaml create mode 100644 .pre-commit-config.yaml delete mode 100644 pyrightconfig.json diff --git a/.pre-commit-config.ruff.yaml b/.pre-commit-config.ruff.yaml new file mode 100644 index 0000000..e687f73 --- /dev/null +++ b/.pre-commit-config.ruff.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e3be59d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.5 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + args: [ --allow-multiple-documents ] + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.18.2' + hooks: + - id: mypy + args: [--strict, --ignore-missing-imports, --check-untyped-defs] + additional_dependencies: + - types-PyYAML diff --git a/Makefile b/Makefile index 42b9a93..d9bc3e7 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: @echo " install - Install the package and dependencies" @echo " install-dev - Install the package and dev dependencies" @echo " test - Run tests" - @echo " format - Format code with black" + @echo " format - Format code with ruff" @echo " lint - Run linting checks" @echo " security - Run security checks with bandit" @echo " docs - Build the documentation" @@ -59,14 +59,9 @@ test-trace: install-test $(POETRY) run pytest -k "not kms" -vvv --log-cli-level=DEBUG format: install-dev - $(POETRY) run black --extend-exclude test-data/gardenlinux . + $(POETRY) run -c .pre-commit-config.ruff.yaml --all-files lint: install-dev - @echo - @echo "------------------------------------------------------------------------------------------------------------------------" - @echo "--// BLACK //-----------------------------------------------------------------------------------------------------------" - @echo "------------------------------------------------------------------------------------------------------------------------" - $(POETRY) run black --diff --extend-exclude test-data/gardenlinux . @echo @echo "------------------------------------------------------------------------------------------------------------------------" @echo "--// ISORT //-----------------------------------------------------------------------------------------------------------" @@ -74,9 +69,9 @@ lint: install-dev $(POETRY) run isort --check-only . @echo @echo "------------------------------------------------------------------------------------------------------------------------" - @echo "--// PYRIGHT //---------------------------------------------------------------------------------------------------------" + @echo "--// PRE-COMMIT //------------------------------------------------------------------------------------------------------" @echo "------------------------------------------------------------------------------------------------------------------------" - $(POETRY) run pyright + $(POETRY) run pre-commit run --all-files security: install-dev @if [ "$(CI)" = "true" ]; then \ diff --git a/docs/index.rst b/docs/index.rst index b3257a8..c6fa838 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,4 +26,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/hack/print_feature_extensions.sh b/hack/print_feature_extensions.sh index 967d57b..5f54dc2 100755 --- a/hack/print_feature_extensions.sh +++ b/hack/print_feature_extensions.sh @@ -3,14 +3,14 @@ search_and_print_directories() { local pattern="$1" - local base_pattern="${pattern%%.*}" - + local base_pattern="${pattern%%.*}" + while IFS= read -r file; do dir=$(dirname "$file" | sed 's|^\./||') - + suffix="${file##*/}" - suffix="${suffix#"$base_pattern"}" - + suffix="${suffix#"$base_pattern"}" + echo "('$dir', '$suffix')," done < <(find . -type f -name "$pattern" | sort -u) } diff --git a/poetry.lock b/poetry.lock index be87687..d2cabdf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -52,14 +52,14 @@ dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)" [[package]] name = "bandit" -version = "1.8.6" +version = "1.9.1" description = "Security oriented static analyser for python code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"}, - {file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"}, + {file = "bandit-1.9.1-py3-none-any.whl", hash = "sha256:0a1f34c04f067ee28985b7854edaa659c9299bd71e1b7e18236e46cccc79720b"}, + {file = "bandit-1.9.1.tar.gz", hash = "sha256:6dbafd1a51e276e065404f06980d624bad142344daeac3b085121fcfd117b7cf"}, ] [package.dependencies] @@ -75,70 +75,20 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] yaml = ["PyYAML"] -[[package]] -name = "black" -version = "25.11.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, - {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, - {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, - {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, - {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, - {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, - {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, - {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, - {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, - {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, - {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, - {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, - {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, - {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, - {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, - {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, - {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, - {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, - {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, - {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, - {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, - {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, - {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, - {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, - {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, - {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.3.0" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "boto3" -version = "1.40.74" +version = "1.40.75" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "boto3-1.40.74-py3-none-any.whl", hash = "sha256:41fc8844b37ae27b24bcabf8369769df246cc12c09453988d0696ad06d6aa9ef"}, - {file = "boto3-1.40.74.tar.gz", hash = "sha256:484e46bf394b03a7c31b34f90945ebe1390cb1e2ac61980d128a9079beac87d4"}, + {file = "boto3-1.40.75-py3-none-any.whl", hash = "sha256:c246fb35d9978b285c5b827a20b81c9e77d52f99c9d175fbd91f14396432953f"}, + {file = "boto3-1.40.75.tar.gz", hash = "sha256:a5219a2f397f8616462d7908e696c281f120aa2d8458280ff24f7ddeb2108faf"}, ] [package.dependencies] -botocore = ">=1.40.74,<1.41.0" +botocore = ">=1.40.75,<1.41.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.14.0,<0.15.0" @@ -147,14 +97,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.40.74" +version = "1.40.75" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "botocore-1.40.74-py3-none-any.whl", hash = "sha256:f39f5763e35e75f0bd91212b7b36120b1536203e8003cd952ef527db79702b15"}, - {file = "botocore-1.40.74.tar.gz", hash = "sha256:57de0b9ffeada06015b3c7e5186c77d0692b210d9e5efa294f3214df97e2f8ee"}, + {file = "botocore-1.40.75-py3-none-any.whl", hash = "sha256:e822004688ca8035c518108e27d5b450d3ab0e0b3a73bcb8b87b80a8e5bd1910"}, + {file = "botocore-1.40.75.tar.gz", hash = "sha256:bf8b067209fee5a9738800d41852e113b8ebdb01bd7f1e8b4541d55ecdbdb8f3"}, ] [package.dependencies] @@ -167,14 +117,14 @@ crt = ["awscrt (==0.28.4)"] [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] @@ -275,6 +225,18 @@ markers = {dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -404,7 +366,7 @@ version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -428,104 +390,104 @@ markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \" [[package]] name = "coverage" -version = "7.11.0" +version = "7.11.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}, - {file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}, - {file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}, - {file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}, - {file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}, - {file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}, - {file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}, - {file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}, - {file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}, - {file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, - {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, - {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, - {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, - {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, - {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, - {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, - {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, - {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, - {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, - {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, - {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, - {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, - {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, - {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, + {file = "coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5"}, + {file = "coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7"}, + {file = "coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb"}, + {file = "coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1"}, + {file = "coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c"}, + {file = "coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31"}, + {file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2"}, + {file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507"}, + {file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832"}, + {file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e"}, + {file = "coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb"}, + {file = "coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8"}, + {file = "coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1"}, + {file = "coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06"}, + {file = "coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80"}, + {file = "coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa"}, + {file = "coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297"}, + {file = "coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362"}, + {file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87"}, + {file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200"}, + {file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4"}, + {file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060"}, + {file = "coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7"}, + {file = "coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55"}, + {file = "coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc"}, + {file = "coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f"}, + {file = "coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e"}, + {file = "coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a"}, + {file = "coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1"}, + {file = "coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd"}, + {file = "coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5"}, + {file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e"}, + {file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044"}, + {file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7"}, + {file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405"}, + {file = "coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e"}, + {file = "coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055"}, + {file = "coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f"}, + {file = "coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36"}, + {file = "coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e"}, + {file = "coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2"}, + {file = "coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63"}, + {file = "coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3"}, + {file = "coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5"}, + {file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5"}, + {file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7"}, + {file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5"}, + {file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094"}, + {file = "coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c"}, + {file = "coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2"}, + {file = "coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944"}, + {file = "coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428"}, + {file = "coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a"}, + {file = "coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655"}, + {file = "coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7"}, + {file = "coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d"}, + {file = "coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f"}, + {file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0"}, + {file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739"}, + {file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71"}, + {file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76"}, + {file = "coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c"}, + {file = "coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac"}, + {file = "coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc"}, + {file = "coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c"}, + {file = "coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203"}, + {file = "coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240"}, + {file = "coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83"}, + {file = "coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902"}, + {file = "coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428"}, + {file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75"}, + {file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704"}, + {file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b"}, + {file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131"}, + {file = "coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a"}, + {file = "coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86"}, + {file = "coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e"}, + {file = "coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df"}, + {file = "coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001"}, + {file = "coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de"}, + {file = "coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926"}, + {file = "coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd"}, + {file = "coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac"}, + {file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46"}, + {file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64"}, + {file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f"}, + {file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820"}, + {file = "coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237"}, + {file = "coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9"}, + {file = "coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd"}, + {file = "coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe"}, + {file = "coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b"}, ] [package.extras] @@ -608,6 +570,18 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -620,6 +594,18 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] +[[package]] +name = "filelock" +version = "3.20.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}, + {file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"}, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -654,6 +640,21 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +[[package]] +name = "identify" +version = "2.6.15" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, + {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.11" @@ -913,14 +914,14 @@ files = [ [[package]] name = "moto" -version = "5.1.16" +version = "5.1.17" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "moto-5.1.16-py3-none-any.whl", hash = "sha256:8e6186f20b3aa91755d186e47701fe7e47f74e625c36fdf3bd7747da68468b19"}, - {file = "moto-5.1.16.tar.gz", hash = "sha256:792045b345d16a8aa09068ad4a7656894e707c796f0799b438fffb738e8fae7c"}, + {file = "moto-5.1.17-py3-none-any.whl", hash = "sha256:7c59e92f6fc50a425b718ab1d490eee570dc0cf3dc7dc3d7ab222b8f1e1cb7fc"}, + {file = "moto-5.1.17.tar.gz", hash = "sha256:31b832de25846529963bcb273448240cae34b9cbaff0bdae15f75c716c08a5f8"}, ] [package.dependencies] @@ -957,18 +958,6 @@ ssm = ["PyYAML (>=5.1)"] stepfunctions = ["antlr4-python3-runtime", "jsonpath_ng"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - [[package]] name = "networkx" version = "3.5" @@ -1036,18 +1025,6 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.5.0" @@ -1081,6 +1058,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.4.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813"}, + {file = "pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pycparser" version = "2.23" @@ -1171,43 +1167,22 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pyright" -version = "1.1.407" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21"}, - {file = "pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" -typing-extensions = ">=4.1" - -[package.extras] -all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] -nodejs = ["nodejs-wheel-binaries"] - [[package]] name = "pytest" -version = "9.0.1" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad"}, - {file = "pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" @@ -1264,21 +1239,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "pytokens" -version = "0.3.0" -description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, - {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, -] - -[package.extras] -dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1475,127 +1435,127 @@ test = ["pytest (>=8)"] [[package]] name = "rpds-py" -version = "0.28.0" +version = "0.29.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a"}, - {file = "rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476"}, - {file = "rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4"}, - {file = "rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457"}, - {file = "rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e"}, - {file = "rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296"}, - {file = "rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0"}, - {file = "rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d"}, - {file = "rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6"}, - {file = "rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa"}, - {file = "rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f"}, - {file = "rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66"}, - {file = "rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5"}, - {file = "rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c"}, - {file = "rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b"}, - {file = "rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d"}, - {file = "rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7"}, - {file = "rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9"}, - {file = "rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e"}, - {file = "rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c"}, - {file = "rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259"}, - {file = "rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37"}, - {file = "rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342"}, - {file = "rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472"}, - {file = "rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d"}, - {file = "rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515"}, - {file = "rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e"}, - {file = "rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b"}, - {file = "rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c"}, - {file = "rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3"}, - {file = "rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829"}, - {file = "rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f"}, - {file = "rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea"}, + {file = "rpds_py-0.29.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4ae4b88c6617e1b9e5038ab3fccd7bac0842fdda2b703117b2aa99bc85379113"}, + {file = "rpds_py-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d9128ec9d8cecda6f044001fde4fb71ea7c24325336612ef8179091eb9596b9"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37812c3da8e06f2bb35b3cf10e4a7b68e776a706c13058997238762b4e07f4f"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66786c3fb1d8de416a7fa8e1cb1ec6ba0a745b2b0eee42f9b7daa26f1a495545"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58f5c77f1af888b5fd1876c9a0d9858f6f88a39c9dd7c073a88e57e577da66d"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:799156ef1f3529ed82c36eb012b5d7a4cf4b6ef556dd7cc192148991d07206ae"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453783477aa4f2d9104c4b59b08c871431647cb7af51b549bbf2d9eb9c827756"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:24a7231493e3c4a4b30138b50cca089a598e52c34cf60b2f35cebf62f274fdea"}, + {file = "rpds_py-0.29.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7033c1010b1f57bb44d8067e8c25aa6fa2e944dbf46ccc8c92b25043839c3fd2"}, + {file = "rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2"}, + {file = "rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b"}, + {file = "rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a"}, + {file = "rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef"}, + {file = "rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950"}, + {file = "rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437"}, + {file = "rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63"}, + {file = "rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2"}, + {file = "rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f"}, + {file = "rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca"}, + {file = "rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95"}, + {file = "rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4"}, + {file = "rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60"}, + {file = "rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c"}, + {file = "rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954"}, + {file = "rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181"}, + {file = "rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c"}, + {file = "rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7"}, + {file = "rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19"}, + {file = "rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0"}, + {file = "rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7"}, + {file = "rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977"}, + {file = "rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7"}, + {file = "rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61"}, + {file = "rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b"}, + {file = "rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55"}, + {file = "rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd"}, + {file = "rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea"}, + {file = "rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22"}, + {file = "rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7"}, + {file = "rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e"}, + {file = "rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2"}, + {file = "rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c"}, + {file = "rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e"}, + {file = "rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb"}, + {file = "rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967"}, + {file = "rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e"}, + {file = "rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a"}, + {file = "rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb"}, + {file = "rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352"}, + {file = "rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1"}, + {file = "rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c"}, + {file = "rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318"}, + {file = "rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212"}, + {file = "rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94"}, + {file = "rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d"}, + {file = "rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1"}, + {file = "rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b"}, + {file = "rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9"}, + {file = "rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10"}, + {file = "rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761"}, + {file = "rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3"}, + {file = "rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9"}, + {file = "rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8"}, + {file = "rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a"}, + {file = "rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5"}, + {file = "rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed"}, + {file = "rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f"}, + {file = "rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359"}, ] [[package]] @@ -1835,18 +1795,6 @@ files = [ {file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"}, ] -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - [[package]] name = "urllib3" version = "2.5.0" @@ -1865,6 +1813,27 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "virtualenv" +version = "20.35.4" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, + {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + [[package]] name = "werkzeug" version = "3.1.3" @@ -1901,4 +1870,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "ea1c3fd1bb706d5202222d76cea0a99b8f1091ea3d3b25532c53f0e48276d8ed" +content-hash = "a9b7af43fcc3d7eee3aa51aca0bc021212de5335d6cac1c9f0805578e84e2b1c" diff --git a/pyproject.toml b/pyproject.toml index d05acce..633909a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,9 @@ packages = [{ include = "gardenlinux", from = "src" }] [tool.poetry.dependencies] python = ">=3.13" apt-repo = "^0.5" -boto3 = "^1.40.57" -click = "^8.2.1" -cryptography = "^46.0.1" +boto3 = "^1.40.74" +click = "^8.3.1" +cryptography = "^46.0.3" jsonschema = "^4.25.1" networkx = "^3.5" oras = "^0.2.38" @@ -23,14 +23,13 @@ gitpython = "^3.1.45" [tool.poetry.group.dev.dependencies] bandit = "^1.8.6" -black = "^25.1.0" -moto = "^5.1.12" -python-dotenv = "^1.1.1" -pytest = "^9.0.0" +moto = "^5.1.16" +pre-commit = "^4.4.0" +python-dotenv = "^1.2.1" +pytest = "^8.4.1" pytest-cov = "^7.0.0" isort = "^7.0.0" requests-mock = "^1.12.1" -pyright = "^1.1.403" [tool.poetry.group.docs.dependencies] sphinx-rtd-theme = "^3.0.2" @@ -53,19 +52,6 @@ line_length = 120 known_first_party = ["gardenlinux"] skip = ["test-data", ".venv", "**/__pycache", ".pytest-cache", "hack"] -[tool.pyright] -typeCheckingMode = "strict" -venvPath = "." -venv = ".venv" -exclude = [ - "test-data", - "docs", - "hack", - ".venv", - ".pytest-cache", - "**/__pycache", -] - [tool.bandit] skips = ["B101", "B404"] # allow asserts, subprocesses exclude_dirs = ["tests"] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 11698df..0000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - // Pyright is configured in 'strict' mode in pyproject.toml - // This file overrides some reports and tones them down to warnings, if they are not critical. - // It also turns some up to become warnings or errors. - "reportShadowedImports": "warning", - "reportImportCycles": "error", - "reportOptionalMemberAccess": "warning", - "reportOptionalSubscript": "warning", - "reportOptionalContextManager": "warning", - "reportOptionalCall": "warning", - "reportUnusedVariable": "warning", - "reportUnusedImport": "warning", - "reportUnusedFunction": "warning", - "reportUnusedCallResult": "none", - "reportUnusedClass": "warning", - "reportUnnecessaryCast": "warning", - "reportUnnecessaryComparison": "warning", - "reportUnnecessaryIsInstance": "warning", - "reportUnnecessaryContains": "warning", - // Becomes useful after introduction of type annotations - // "reportUnknownMemberType": "warning", - // "reportUnknownParameterType": "warning", - // "reportUnknownArgumentType": "warning", - // "reportUnknownVariableType": "warning", - "reportTypedDictNotRequiredAccess": "error", - "reportDeprecated": "warning", -} \ No newline at end of file diff --git a/src/gardenlinux/apt/debsource.py b/src/gardenlinux/apt/debsource.py index aacfcb7..0fceaf0 100644 --- a/src/gardenlinux/apt/debsource.py +++ b/src/gardenlinux/apt/debsource.py @@ -21,7 +21,7 @@ class Debsrc: Apache License, Version 2.0 """ - def __init__(self, deb_source, deb_version): + def __init__(self, deb_source: str, deb_version: str): """ Constructor __init__(Debsrc) @@ -31,8 +31,8 @@ def __init__(self, deb_source, deb_version): :since: 0.7.0 """ - self.deb_source: str = deb_source - self.deb_version: str = deb_version + self.deb_source = deb_source + self.deb_version = deb_version def __repr__(self) -> str: """ diff --git a/src/gardenlinux/apt/package_repo_info.py b/src/gardenlinux/apt/package_repo_info.py index 5342ed1..d577b75 100644 --- a/src/gardenlinux/apt/package_repo_info.py +++ b/src/gardenlinux/apt/package_repo_info.py @@ -9,7 +9,7 @@ from apt_repo import APTRepository -class GardenLinuxRepo(APTRepository): +class GardenLinuxRepo(APTRepository): # type: ignore[misc] """ Class to reflect APT based GardenLinux repositories. diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index 2edf4fb..dee5eca 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -8,7 +8,7 @@ from configparser import UNNAMED_SECTION, ConfigParser from os import PathLike, environ from pathlib import Path -from typing import List, Optional +from typing import Optional from ..constants import ( ARCHS, diff --git a/src/gardenlinux/features/parser.py b/src/gardenlinux/features/parser.py index 9300f6a..1b0d3f4 100644 --- a/src/gardenlinux/features/parser.py +++ b/src/gardenlinux/features/parser.py @@ -9,7 +9,7 @@ from functools import reduce from glob import glob from pathlib import Path -from typing import Callable, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, Set import networkx import yaml @@ -39,7 +39,7 @@ class Parser(object): def __init__( self, gardenlinux_root: Optional[str] = None, - feature_dir_name: Optional[str] = "features", + feature_dir_name: str = "features", logger: Optional[logging.Logger] = None, ): """ @@ -117,7 +117,7 @@ def graph(self) -> networkx.Graph: def filter( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> networkx.Graph: @@ -140,10 +140,10 @@ def filter( def filter_as_dict( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, - ) -> dict: + ) -> Dict[str, List[str]]: """ Filters the features graph and returns it as a dict. @@ -160,10 +160,10 @@ def filter_as_dict( def filter_as_list( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, - ) -> list: + ) -> List[str]: """ Filters the features graph and returns it as a list. @@ -180,7 +180,7 @@ def filter_as_list( def filter_as_string( self, - cname: str | None, + cname: str, ignore_excludes: bool = False, additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> str: @@ -201,7 +201,7 @@ def filter_as_string( def filter_graph_as_dict( self, graph: networkx.Graph, - ) -> dict: + ) -> Dict[str, List[str]]: """ Filters the features graph and returns it as a dict. @@ -213,7 +213,7 @@ def filter_graph_as_dict( features = Parser.sort_reversed_graph_nodes(graph) - features_by_type = {} + features_by_type: Dict[str, List[str]] = {} for feature in features: node_type = Parser._get_graph_node_type(graph.nodes[feature]) @@ -228,7 +228,7 @@ def filter_graph_as_dict( def filter_graph_as_list( self, graph: networkx.Graph, - ) -> list: + ) -> List[str]: """ Filters the features graph and returns it as a list. @@ -258,9 +258,9 @@ def filter_graph_as_string( def filter_based_on_feature_set( self, - feature_set: (str,), + feature_set: List[str], ignore_excludes: bool = False, - additional_filter_func: Optional[Callable[(str,), bool]] = None, + additional_filter_func: Optional[Callable[[str], bool]] = None, ) -> networkx.Graph: """ Filters the features graph based on a feature set given. @@ -301,7 +301,9 @@ def filter_based_on_feature_set( return graph - def _exclude_from_filter_set(graph, feature_set, filter_set): + def _exclude_from_filter_set( + graph: networkx.Graph, feature_set: List[str], filter_set: List[str] + ) -> None: """ Removes the given `filter_set` out of `feature_set`. @@ -331,7 +333,7 @@ def _exclude_from_filter_set(graph, feature_set, filter_set): if exclude_graph_view.edges(): raise ValueError("Including explicitly excluded feature") - def _get_node_features(self, node): + def _get_node_features(self, node: Dict[str, Any]) -> Dict[str, Any]: """ Returns the features for a given features node. @@ -341,9 +343,9 @@ def _get_node_features(self, node): :since: 0.7.0 """ - return node.get("content", {}).get("features", {}) + return node.get("content", {}).get("features", {}) # type: ignore[no-any-return] - def _read_feature_yaml(self, feature_yaml_file: str): + def _read_feature_yaml(self, feature_yaml_file: str) -> Dict[str, Any]: """ Reads and returns the content of the given features file. @@ -361,7 +363,7 @@ def _read_feature_yaml(self, feature_yaml_file: str): return {"name": name, "content": content} @staticmethod - def get_flavor_from_feature_set(sorted_features: List[str]): + def get_flavor_from_feature_set(sorted_features: List[str]) -> str: """ Get the base cname for the feature set given. @@ -376,7 +378,7 @@ def get_flavor_from_feature_set(sorted_features: List[str]): ) @staticmethod - def get_flavor_as_feature_set(cname): + def get_flavor_as_feature_set(cname: str) -> List[str]: """ Returns the features of a given canonical name. @@ -402,10 +404,13 @@ def get_flavor_as_feature_set(cname): else: features.append(feature) - return [platform] + sorted(features) + sorted(flags) + return [platform] + sorted(features) + sorted(flags) # type: ignore[return-value] @staticmethod - def _get_filter_set_callable(filter_set, additional_filter_func): + def _get_filter_set_callable( + filter_set: List[str], + additional_filter_func: Optional[Callable[[str], bool]] = None, + ) -> Callable[[str], bool]: """ Returns the filter function used for the graph. @@ -416,16 +421,17 @@ def _get_filter_set_callable(filter_set, additional_filter_func): :since: 0.7.0 """ - def filter_func(node): + def filter_func(node: str) -> bool: additional_filter_result = ( True if additional_filter_func is None else additional_filter_func(node) ) + return node in filter_set and additional_filter_result return filter_func @staticmethod - def _get_graph_view_for_attr(graph, attr): + def _get_graph_view_for_attr(graph: networkx.Graph, attr: str) -> networkx.Graph: """ Returns a graph view to return `attr` data. @@ -441,7 +447,9 @@ def _get_graph_view_for_attr(graph, attr): ) @staticmethod - def _get_graph_view_for_attr_callable(graph, attr): + def _get_graph_view_for_attr_callable( + graph: networkx.Graph, attr: str + ) -> Callable[[str, str], bool]: """ Returns the filter function used to filter for `attr` data. @@ -452,13 +460,13 @@ def _get_graph_view_for_attr_callable(graph, attr): :since: 0.7.0 """ - def filter_func(a, b): - return graph.get_edge_data(a, b)["attr"] == attr + def filter_func(a: str, b: str) -> bool: + return graph.get_edge_data(a, b)["attr"] == attr # type: ignore[no-any-return] return filter_func @staticmethod - def _get_graph_node_type(node): + def _get_graph_node_type(node: str) -> str: """ Returns the node feature type. @@ -468,10 +476,10 @@ def _get_graph_node_type(node): :since: 0.7.0 """ - return node.get("content", {}).get("type") + return node.get("content", {}).get("type") # type: ignore[attr-defined, no-any-return] @staticmethod - def set_default_gardenlinux_root_dir(root_dir): + def set_default_gardenlinux_root_dir(root_dir: str) -> None: """ Sets the default GardenLinux root directory used. @@ -483,7 +491,7 @@ def set_default_gardenlinux_root_dir(root_dir): Parser._GARDENLINUX_ROOT = root_dir @staticmethod - def sort_graph_nodes(graph): + def sort_graph_nodes(graph: networkx.Graph) -> List[str]: """ Sorts graph nodes by feature type. @@ -493,7 +501,7 @@ def sort_graph_nodes(graph): :since: 0.7.0 """ - def key_function(node): + def key_function(node: str) -> str: prefix_map = {"platform": "0", "element": "1", "flag": "2"} node_type = Parser._get_graph_node_type(graph.nodes.get(node, {})) prefix = prefix_map[node_type] @@ -517,7 +525,7 @@ def subset(input_set: Set[str], order_list: List[str]) -> List[str]: return [item for item in order_list if item in input_set] @staticmethod - def sort_reversed_graph_nodes(graph): + def sort_reversed_graph_nodes(graph: networkx.Graph) -> List[str]: """ Sorts graph nodes by feature type. diff --git a/src/gardenlinux/flavors/parser.py b/src/gardenlinux/flavors/parser.py index f7eec9e..b71eeb2 100644 --- a/src/gardenlinux/flavors/parser.py +++ b/src/gardenlinux/flavors/parser.py @@ -5,6 +5,8 @@ """ import fnmatch +from logging import Logger +from typing import Dict, List, Optional import yaml from jsonschema import validate as jsonschema_validate @@ -26,7 +28,7 @@ class Parser(object): Apache License, Version 2.0 """ - def __init__(self, data, logger=None): + def __init__(self, data: str, logger: Optional[Logger] = None): """ Constructor __init__(Parser) @@ -51,15 +53,15 @@ def __init__(self, data, logger=None): def filter( self, - include_only_patterns=[], - wildcard_excludes=[], - only_build=False, - only_test=False, - only_test_platform=False, - only_publish=False, - filter_categories=[], - exclude_categories=[], - ): + include_only_patterns: List[str] = [], + wildcard_excludes: List[str] = [], + only_build: bool = False, + only_test: bool = False, + only_test_platform: bool = False, + only_publish: bool = False, + filter_categories: List[str] = [], + exclude_categories: List[str] = [], + ) -> List[str]: """ Filters flavors data and generates combinations. @@ -128,11 +130,12 @@ def filter( combinations.append((arch, combination)) return sorted( - combinations, key=lambda platform: platform[1].split("-")[0] + combinations, # type: ignore[arg-type] + key=lambda platform: platform[1].split("-")[0], ) # Sort by platform name @staticmethod - def group_by_arch(combinations): + def group_by_arch(combinations: Dict[str, List[str]]) -> Dict[str, List[str]]: """ Groups combinations by architecture into a dictionary. @@ -142,15 +145,18 @@ def group_by_arch(combinations): :since: 0.7.0 """ - arch_dict = {} - for arch, combination in combinations: - arch_dict.setdefault(arch, []).append(combination) + arch_dict: Dict[str, List[str]] = {} + + for arch, combination in combinations: # type: ignore[misc] + arch_dict.setdefault(arch, []).append(combination) # type: ignore[has-type] + for arch in arch_dict: - arch_dict[arch] = sorted(set(arch_dict[arch])) # Deduplicate and sort + arch_dict[arch] = sorted(set(arch_dict[arch])) + return arch_dict @staticmethod - def remove_arch(combinations): + def remove_arch(combinations: Dict[str, List[str]]) -> List[str]: """ Removes the architecture from combinations. @@ -160,16 +166,19 @@ def remove_arch(combinations): :since: 0.7.0 """ - return [ - combination.replace(f"-{arch}", "") for arch, combination in combinations + return [ # type: ignore[misc] + combination.replace(f"-{arch}", "") # type: ignore[has-type] + for arch, combination in combinations ] @staticmethod - def should_exclude(combination, excludes, wildcard_excludes): + def should_exclude( + combination: str, excludes: List[str], wildcard_excludes: List[str] + ) -> bool: """ Checks if a combination should be excluded based on exact match or wildcard patterns. - :param combinations: Flavor combinations + :param combination: Feature :param excludes: List of features to exclude :param wildcard_excludes: List of feature wildcards to exclude @@ -186,12 +195,12 @@ def should_exclude(combination, excludes, wildcard_excludes): ) @staticmethod - def should_include_only(combination, include_only_patterns): + def should_include_only(combination: str, include_only_patterns: List[str]) -> bool: """ Checks if a combination should be included based on `--include-only` wildcard patterns. If no patterns are provided, all combinations are included by default. - :param combinations: Flavor combinations + :param combination: Feature :param include_only_patterns: List of features to include :return: (bool) True if included diff --git a/src/gardenlinux/git/repository.py b/src/gardenlinux/git/repository.py index 3af9190..c5972bc 100755 --- a/src/gardenlinux/git/repository.py +++ b/src/gardenlinux/git/repository.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from os import PathLike +from logging import Logger from pathlib import Path +from typing import Any, List, Optional from pygit2 import Oid from pygit2 import Repository as _Repository @@ -11,7 +13,7 @@ from ..logger import LoggerSetup -class Repository(_Repository): +class Repository(_Repository): # type: ignore[misc] """ Repository operations handler based on the given Git directory. @@ -24,7 +26,12 @@ class Repository(_Repository): Apache License, Version 2.0 """ - def __init__(self, git_directory: str | PathLike[str] = ".", logger=None, **kwargs): + def __init__( + self, + git_directory: PathLike[str] | str = ".", + logger: Optional[Logger] = None, + **kwargs: Any, + ): """ Constructor __init__(Repository) @@ -48,7 +55,7 @@ def __init__(self, git_directory: str | PathLike[str] = ".", logger=None, **kwar self._logger = logger @property - def commit_id(self): + def commit_id(self) -> str: """ Returns the commit ID for Git `HEAD`. @@ -59,7 +66,7 @@ def commit_id(self): return str(self.root_repo.head.target) @property - def root(self): + def root(self) -> Path: """ Returns the root directory of the current Git repository. @@ -88,7 +95,7 @@ def root(self): return Path(root_dir) @property - def root_repo(self): + def root_repo(self) -> Any: """ Returns the root Git `Repository` instance. @@ -105,14 +112,14 @@ def root_repo(self): @staticmethod def checkout_repo( - git_directory: str | PathLike[str], - repo_url=GL_REPOSITORY_URL, + git_directory: PathLike[str] | str, + repo_url: str = GL_REPOSITORY_URL, branch: str = "main", - commit: str | None = None, - pathspecs: list[str] | None = None, - logger=None, - **kwargs, - ): + commit: Optional[str] = None, + pathspecs: Optional[List[str]] = None, + logger: Optional[Logger] = None, + **kwargs: Any, + ) -> Any: """ Returns the root Git `Repo` instance. @@ -145,14 +152,14 @@ def checkout_repo( @staticmethod def checkout_repo_sparse( - git_directory, - pathspecs=[], - repo_url=GL_REPOSITORY_URL, - branch="main", - commit=None, - logger=None, - **kwargs, - ): + git_directory: PathLike[str] | str, + pathspecs: List[str] = [], + repo_url: str = GL_REPOSITORY_URL, + branch: str = "main", + commit: Optional[str] = None, + logger: Optional[Logger] = None, + **kwargs: Any, + ) -> Any: """ Sparse checkout given Git repository and return the `Repository` instance. diff --git a/src/gardenlinux/logger.py b/src/gardenlinux/logger.py index 158c85c..c362ac7 100644 --- a/src/gardenlinux/logger.py +++ b/src/gardenlinux/logger.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- import logging +from typing import Optional class LoggerSetup: """Handles logging configuration for the gardenlinux library.""" @staticmethod - def get_logger(name, level=None): + def get_logger(name: str, level: Optional[int] = None) -> logging.Logger: """Create and configure a logger. Args: diff --git a/src/gardenlinux/oci/container.py b/src/gardenlinux/oci/container.py index 28b581a..9708b97 100644 --- a/src/gardenlinux/oci/container.py +++ b/src/gardenlinux/oci/container.py @@ -283,7 +283,7 @@ def push_index_from_directory( if manifest["digest"] == existing_manifest["digest"]: self._logger.debug( - f"Skipping manifest with digest {manifest["digest"]} - already exists" + f"Skipping manifest with digest {manifest['digest']} - already exists" ) continue @@ -291,7 +291,7 @@ def push_index_from_directory( index.append_manifest(manifest) self._logger.info( - f"Index appended locally {manifest["annotations"]["cname"]}" + f"Index appended locally {manifest['annotations']['cname']}" ) new_entries += 1 @@ -440,7 +440,7 @@ def push_manifest_and_artifacts( ) self._logger.info( - f"Pushed {artifact["file_name"]}: {layer_dict["digest"]}" + f"Pushed {artifact['file_name']}: {layer_dict['digest']}" ) finally: if cleanup_blob and file_path_name.exists(): diff --git a/src/gardenlinux/oci/index.py b/src/gardenlinux/oci/index.py index 94a3143..1432798 100644 --- a/src/gardenlinux/oci/index.py +++ b/src/gardenlinux/oci/index.py @@ -2,11 +2,12 @@ import json from copy import deepcopy +from typing import Any, Dict from .schemas import EmptyIndex -class Index(dict): +class Index(dict): # type: ignore[type-arg] """ OCI image index @@ -19,7 +20,7 @@ class Index(dict): Apache License, Version 2.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): """ Constructor __init__(Index) @@ -33,7 +34,7 @@ def __init__(self, *args, **kwargs): self.update(**kwargs) @property - def json(self): + def json(self) -> bytes: """ Returns the OCI image index as a JSON @@ -44,7 +45,7 @@ def json(self): return json.dumps(self).encode("utf-8") @property - def manifests_as_dict(self): + def manifests_as_dict(self) -> Dict[str, Dict[str, Any]]: """ Returns the OCI image manifests of the index @@ -64,7 +65,7 @@ def manifests_as_dict(self): return manifests - def append_manifest(self, manifest): + def append_manifest(self, manifest: Dict[str, Any]) -> None: """ Appends the given OCI image manifest to the index diff --git a/src/gardenlinux/oci/layer.py b/src/gardenlinux/oci/layer.py index c2ec7b7..c01e01e 100644 --- a/src/gardenlinux/oci/layer.py +++ b/src/gardenlinux/oci/layer.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from os import PathLike from pathlib import Path -from typing import Optional +from typing import Any, Dict, Iterator, Optional from oras.defaults import annotation_title as ANNOTATION_TITLE from oras.oci import Layer as _Layer @@ -13,7 +13,7 @@ _SUPPORTED_MAPPING_KEYS = ("annotations",) -class Layer(_Layer, Mapping): +class Layer(_Layer, Mapping): # type: ignore[misc, type-arg] """ OCI image layer @@ -28,7 +28,7 @@ class Layer(_Layer, Mapping): def __init__( self, - blob_path: PathLike | str, + blob_path: PathLike[str] | str, media_type: Optional[str] = None, is_dir: bool = False, ): @@ -48,23 +48,24 @@ def __init__( _Layer.__init__(self, blob_path, media_type, is_dir) self._annotations = { - ANNOTATION_TITLE: blob_path.name, + ANNOTATION_TITLE: blob_path.name, # type: ignore[attr-defined] } @property - def dict(self): + def dict(self) -> Dict[Any, Any]: """ Return a dictionary representation of the layer :return: (dict) OCI manifest layer metadata dictionary :since: 0.7.2 """ + layer = _Layer.to_dict(self) layer["annotations"] = self._annotations - return layer + return layer # type: ignore[no-any-return] - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: """ python.org: Called to implement deletion of self[key]. @@ -80,7 +81,7 @@ def __delitem__(self, key): f"'{self.__class__.__name__}' object is not subscriptable except for keys: {_SUPPORTED_MAPPING_KEYS}" ) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: """ python.org: Called to implement evaluation of self[key]. @@ -97,7 +98,7 @@ def __getitem__(self, key): f"'{self.__class__.__name__}' object is not subscriptable except for keys: {_SUPPORTED_MAPPING_KEYS}" ) - def __iter__(self): + def __iter__(self) -> Iterator[str]: """ python.org: Return an iterator object. @@ -107,7 +108,7 @@ def __iter__(self): return iter(_SUPPORTED_MAPPING_KEYS) - def __len__(self): + def __len__(self) -> int: """ python.org: Called to implement the built-in function len(). @@ -117,7 +118,7 @@ def __len__(self): return len(_SUPPORTED_MAPPING_KEYS) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: """ python.org: Called to implement assignment to self[key]. @@ -135,7 +136,9 @@ def __setitem__(self, key, value): ) @staticmethod - def generate_metadata_from_file_name(file_name: PathLike | str, arch: str) -> dict: + def generate_metadata_from_file_name( + file_name: PathLike[str] | str, arch: str + ) -> Dict[str, Any]: """ Generates OCI manifest layer metadata for the given file path and name. @@ -152,13 +155,13 @@ def generate_metadata_from_file_name(file_name: PathLike | str, arch: str) -> di media_type = Layer.lookup_media_type_for_file_name(file_name) return { - "file_name": file_name.name, + "file_name": file_name.name, # type: ignore[attr-defined] "media_type": media_type, "annotations": {"io.gardenlinux.image.layer.architecture": arch}, } @staticmethod - def lookup_media_type_for_file_name(file_name: str) -> str: + def lookup_media_type_for_file_name(file_name: PathLike[str] | str) -> str: """ Looks up the media type based on file name or extension. @@ -172,7 +175,7 @@ def lookup_media_type_for_file_name(file_name: str) -> str: file_name = Path(file_name) for lookup_name in GL_MEDIA_TYPES: - if file_name.match(f"*.{lookup_name}") or file_name.name == lookup_name: + if file_name.match(f"*.{lookup_name}") or file_name.name == lookup_name: # type: ignore[attr-defined] return GL_MEDIA_TYPE_LOOKUP[lookup_name] raise ValueError( diff --git a/src/gardenlinux/oci/manifest.py b/src/gardenlinux/oci/manifest.py index ed52182..8a877bf 100644 --- a/src/gardenlinux/oci/manifest.py +++ b/src/gardenlinux/oci/manifest.py @@ -3,12 +3,13 @@ import json from copy import deepcopy from hashlib import sha256 +from typing import Any, Dict from oras.defaults import unknown_config_media_type as UNKNOWN_CONFIG_MEDIA_TYPE from oras.oci import EmptyManifest -class Manifest(dict): +class Manifest(dict): # type: ignore[type-arg] """ OCI image manifest @@ -21,7 +22,7 @@ class Manifest(dict): Apache License, Version 2.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): """ Constructor __init__(Manifest) @@ -37,7 +38,7 @@ def __init__(self, *args, **kwargs): self.update(**kwargs) @property - def commit(self): + def commit(self) -> str: """ Returns the GardenLinux Git commit ID of the OCI manifest. @@ -50,10 +51,10 @@ def commit(self): "Unexpected manifest with missing config annotation 'commit' found" ) - return self["annotations"]["commit"] + return self["annotations"]["commit"] # type: ignore[no-any-return] @commit.setter - def commit(self, value): + def commit(self, value: str) -> None: """ Sets the GardenLinux Git commit ID of the OCI manifest. @@ -66,7 +67,7 @@ def commit(self, value): self["annotations"]["commit"] = value @property - def config_json(self): + def config_json(self) -> bytes: """ Returns the OCI image manifest config. @@ -77,7 +78,7 @@ def config_json(self): return self._config_bytes @property - def digest(self): + def digest(self) -> str: """ Returns the OCI image manifest digest. @@ -89,7 +90,7 @@ def digest(self): return f"sha256:{digest}" @property - def json(self): + def json(self) -> bytes: """ Returns the OCI image manifest as a JSON @@ -100,7 +101,7 @@ def json(self): return json.dumps(self).encode("utf-8") @property - def size(self): + def size(self) -> int: """ Returns the OCI image manifest JSON size in bytes. @@ -111,7 +112,7 @@ def size(self): return len(self.json) @property - def version(self): + def version(self) -> str: """ Returns the GardenLinux version of the OCI image manifest. @@ -124,10 +125,10 @@ def version(self): "Unexpected manifest with missing config annotation 'version' found" ) - return self["annotations"]["version"] + return self["annotations"]["version"] # type: ignore[no-any-return] @version.setter - def version(self, value): + def version(self, value: str) -> None: """ Sets the GardenLinux version of the OCI image manifest. @@ -139,7 +140,9 @@ def version(self, value): self._ensure_annotations_dict() self["annotations"]["version"] = value - def config_from_dict(self, config: dict, annotations: dict): + def config_from_dict( + self, config: Dict[str, Any], annotations: Dict[str, Any] + ) -> None: """ Write a new OCI configuration to file, and generate oci metadata for it. @@ -162,6 +165,6 @@ def config_from_dict(self, config: dict, annotations: dict): self["config"] = config - def _ensure_annotations_dict(self): + def _ensure_annotations_dict(self) -> None: if "annotations" not in self: self["annotations"] = {} diff --git a/src/gardenlinux/oci/platform.py b/src/gardenlinux/oci/platform.py index f408569..95dc336 100644 --- a/src/gardenlinux/oci/platform.py +++ b/src/gardenlinux/oci/platform.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from copy import deepcopy +from typing import Any, Dict from .schemas import EmptyPlatform -def NewPlatform(architecture: str, version: str) -> dict: +def NewPlatform(architecture: str, version: str) -> Dict[str, Any]: platform = deepcopy(EmptyPlatform) platform["architecture"] = architecture platform["os.version"] = version diff --git a/src/gardenlinux/s3/bucket.py b/src/gardenlinux/s3/bucket.py index 403e8c9..8514225 100644 --- a/src/gardenlinux/s3/bucket.py +++ b/src/gardenlinux/s3/bucket.py @@ -9,7 +9,7 @@ from os import PathLike from pathlib import Path from time import time -from typing import Any, Optional +from typing import Any, BinaryIO, List, Optional import boto3 @@ -62,7 +62,7 @@ def __init__( self._logger = logger @property - def objects(self): + def objects(self) -> List[Any]: """ Returns a list of all objects in a bucket. @@ -72,9 +72,9 @@ def objects(self): self._logger.debug(f"Returning all S3 bucket objects for {self._bucket.name}") - return self._bucket.objects.all() + return self._bucket.objects.all() # type: ignore[no-any-return] - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: """ python.org: Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the @@ -88,7 +88,9 @@ class tree for self). return getattr(self._bucket, name) - def download_file(self, key, file_name, *args, **kwargs): + def download_file( + self, key: str, file_name: str, *args: Any, **kwargs: Any + ) -> None: """ boto3: Download an S3 object to a file. @@ -102,7 +104,9 @@ def download_file(self, key, file_name, *args, **kwargs): self._logger.info(f"Downloaded {key} from S3 to {file_name}") - def download_fileobj(self, key, fp, *args, **kwargs): + def download_fileobj( + self, key: str, fp: BinaryIO, *args: Any, **kwargs: Any + ) -> None: """ boto3: Download an object from this bucket to a file-like-object. @@ -117,8 +121,11 @@ def download_fileobj(self, key, fp, *args, **kwargs): self._logger.info(f"Downloaded {key} from S3 as binary data") def read_cache_file_or_filter( - self, cache_file: str | PathLike[str] | None, cache_ttl: int = 3600, **kwargs - ): + self, + cache_file: Optional[PathLike[str] | str], + cache_ttl: int = 3600, + **kwargs: Any, + ) -> List[Any]: """ Read S3 object keys from cache if valid or filter for S3 object keys. @@ -138,7 +145,7 @@ def read_cache_file_or_filter( and (time() - cache_path.stat().st_mtime) < cache_ttl ): with cache_path.open("r") as fp: - return json.loads(fp.read()) + return json.loads(fp.read()) # type: ignore[no-any-return] else: cache_path = None @@ -152,7 +159,7 @@ def read_cache_file_or_filter( return artifacts - def upload_file(self, file_name, key, *args, **kwargs): + def upload_file(self, file_name: str, key: str, *args: Any, **kwargs: Any) -> None: """ boto3: Upload a file to an S3 object. @@ -166,7 +173,7 @@ def upload_file(self, file_name, key, *args, **kwargs): self._logger.info(f"Uploaded {key} to S3 for {file_name}") - def upload_fileobj(self, fp, key, *args, **kwargs): + def upload_fileobj(self, fp: BinaryIO, key: str, *args: Any, **kwargs: Any) -> None: """ boto3: Upload a file-like object to this bucket. diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index 4d63338..ddf639c 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -66,11 +66,7 @@ def bucket(self): return self._bucket - def download_to_directory( - self, - cname: str, - artifacts_dir: str | PathLike[str], - ): + def download_to_directory(self, cname: str, artifacts_dir: PathLike[str] | str): """ Download S3 artifacts to a given directory. @@ -101,7 +97,7 @@ def download_to_directory( def upload_from_directory( self, cname: str, - artifacts_dir: str | PathLike[str], + artifacts_dir: PathLike[str] | str, delete_before_push=False, dry_run=False, ): diff --git a/tests/apt/test_debsource.py b/tests/apt/test_debsource.py index cf7988a..2e9ac2a 100644 --- a/tests/apt/test_debsource.py +++ b/tests/apt/test_debsource.py @@ -86,7 +86,7 @@ """ -def test_parse_debsource_file(): +def test_parse_debsource_file() -> None: expected = sorted( [ "vim-common 2:9.1.0496-1", diff --git a/tests/apt/test_package_repo_info.py b/tests/apt/test_package_repo_info.py index 0742e00..e9f8d1e 100644 --- a/tests/apt/test_package_repo_info.py +++ b/tests/apt/test_package_repo_info.py @@ -1,6 +1,9 @@ from types import SimpleNamespace +from typing import List import gardenlinux.apt.package_repo_info as repoinfo +import pytest +from apt_repo import BinaryPackage class FakeAPTRepo: @@ -11,18 +14,18 @@ class FakeAPTRepo: - exposes `.packages` and `get_packages_by_name(name)` """ - def __init__(self, url, dist, components) -> None: + def __init__(self, url: str, dist: str, components: List[str]) -> None: self.url = url self.dist = dist self.components = components # list of objects with .package and .version attributes - self.packages = [] + self.packages: List[BinaryPackage] = [] - def get_packages_by_name(self, name): + def get_packages_by_name(self, name: str) -> BinaryPackage: return [p for p in self.packages if p.package == name] -def test_gardenlinuxrepo_init(monkeypatch): +def test_gardenlinuxrepo_init(monkeypatch: pytest.monkeypatch) -> None: """ Test if GardenLinuxRepo creates an internal APTRepo """ @@ -44,7 +47,7 @@ def test_gardenlinuxrepo_init(monkeypatch): assert gr.repo.components == gr.components -def test_get_package_version_by_name(monkeypatch): +def test_get_package_version_by_name(monkeypatch: pytest.monkeypatch) -> None: # Arrange monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo) gr = repoinfo.GardenLinuxRepo("d") @@ -52,7 +55,7 @@ def test_get_package_version_by_name(monkeypatch): gr.repo.packages = [ SimpleNamespace(package="pkg-a", version="1.0"), SimpleNamespace(package="pkg-b", version="2.0"), - ] # type: ignore + ] # Act result = gr.get_package_version_by_name("pkg-a") @@ -61,14 +64,16 @@ def test_get_package_version_by_name(monkeypatch): assert result == [("pkg-a", "1.0")] -def test_get_packages_versions_returns_all_pairs(monkeypatch): +def test_get_packages_versions_returns_all_pairs( + monkeypatch: pytest.monkeypatch, +) -> None: # Arrange monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo) gr = repoinfo.GardenLinuxRepo("d") gr.repo.packages = [ SimpleNamespace(package="aa", version="0.1"), SimpleNamespace(package="bb", version="0.2"), - ] # type: ignore + ] # Act pv = gr.get_packages_versions() @@ -77,7 +82,7 @@ def test_get_packages_versions_returns_all_pairs(monkeypatch): assert pv == [("aa", "0.1"), ("bb", "0.2")] -def test_compare_repo_union_returns_all(): +def test_compare_repo_union_returns_all() -> None: """ When available_in_both=False, compare_repo returns entries for: - only names in A @@ -100,7 +105,7 @@ def test_compare_repo_union_returns_all(): assert set(result) == expected -def test_compare_repo_intersection_only(): +def test_compare_repo_intersection_only() -> None: """ When available_in_both=True, only intersection names are considered; differences are only returned if versions differ. @@ -116,7 +121,7 @@ def test_compare_repo_intersection_only(): assert set(result) == {("b", "2", "3")} -def test_compare_same_returns_empty(): +def test_compare_same_returns_empty() -> None: """ When both sets are identical, compare_repo should return an empty set. """ @@ -128,7 +133,7 @@ def test_compare_same_returns_empty(): assert repoinfo.compare_repo(a, b, available_in_both=False) == [] # type: ignore -def test_compare_empty_returns_empty(): +def test_compare_empty_returns_empty() -> None: """ If both sets are empty, compare_repo should return an empty set. """ diff --git a/tests/features/test_cname_main.py b/tests/features/test_cname_main.py index 2362542..453c1c2 100644 --- a/tests/features/test_cname_main.py +++ b/tests/features/test_cname_main.py @@ -17,7 +17,9 @@ def test_main_happy(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", argv) class FakeGraph: - in_degree = lambda self: [("f1", 0)] + def in_degree(self): + return [("f1", 0)] + edges = [("f1", "f2")] class FakeParser(Parser): diff --git a/tests/features/test_main.py b/tests/features/test_main.py index d10d862..9101b6f 100644 --- a/tests/features/test_main.py +++ b/tests/features/test_main.py @@ -4,7 +4,7 @@ import pytest import gardenlinux.features.__main__ as fema -from gardenlinux.features import CName, Parser +from gardenlinux.features import CName from ..constants import GL_ROOT_DIR diff --git a/tests/github/test_create_github_release.py b/tests/github/test_create_github_release.py index c876729..23af349 100644 --- a/tests/github/test_create_github_release.py +++ b/tests/github/test_create_github_release.py @@ -21,9 +21,9 @@ def test_create_github_release_needs_github_token(): False, "", ) - assert ( - str(exn.value) == "GITHUB_TOKEN environment variable not set" - ), "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + assert str(exn.value) == "GITHUB_TOKEN environment variable not set", ( + "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + ) def test_create_github_release_raise_on_failure(caplog, github_token): @@ -81,6 +81,6 @@ def test_write_to_release_id_file_broken_file_permissions(release_id_file, caplo with pytest.raises(SystemExit): write_to_release_id_file(TEST_GARDENLINUX_RELEASE) - assert any( - "Could not create" in record.message for record in caplog.records - ), "Expected a failure log record" + assert any("Could not create" in record.message for record in caplog.records), ( + "Expected a failure log record" + ) diff --git a/tests/github/test_create_github_release_notes.py b/tests/github/test_create_github_release_notes.py index 38fc56b..55b08a7 100644 --- a/tests/github/test_create_github_release_notes.py +++ b/tests/github/test_create_github_release_notes.py @@ -36,9 +36,9 @@ def test_release_notes_changes_section_empty_packagelist(): text='{"packageList": []}', status_code=200, ) - assert ( - release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "" - ), "Expected an empty result if GLVD returns an empty package list" + assert release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "", ( + "Expected an empty result if GLVD returns an empty package list" + ) def test_release_notes_changes_section_broken_glvd_response(): @@ -50,7 +50,9 @@ def test_release_notes_changes_section_broken_glvd_response(): ) assert "fill this in" in release_notes_changes_section( TEST_GARDENLINUX_RELEASE - ), "Expected a placeholder message to be generated if GVLD response is not valid" + ), ( + "Expected a placeholder message to be generated if GVLD response is not valid" + ) def test_release_notes_compare_package_versions_section_legacy_versioning_is_recognized(): @@ -121,7 +123,6 @@ def test_default_get_file_extension_for_deployment_platform(): @mock_aws def test_github_release_page(monkeypatch, downloads_dir, release_s3_bucket): - class SubmoduleAsRepo(Repo): """This will fake a git submodule as a git repository object.""" @@ -169,10 +170,12 @@ def __new__(cls, *args, **kwargs): text=glvd_response_fixture_path.read_text(), status_code=200, ) - generated_release_notes = gardenlinux.github.release_notes.create_github_release_notes( # pyright: ignore[reportAttributeAccessIssue] - TEST_GARDENLINUX_RELEASE, - TEST_GARDENLINUX_COMMIT, - release_s3_bucket.name, + generated_release_notes = ( + gardenlinux.github.release_notes.create_github_release_notes( # pyright: ignore[reportAttributeAccessIssue] + TEST_GARDENLINUX_RELEASE, + TEST_GARDENLINUX_COMMIT, + release_s3_bucket.name, + ) ) assert generated_release_notes == release_fixture_path.read_text() diff --git a/tests/github/test_github_script.py b/tests/github/test_github_script.py index 14c5569..03f279e 100644 --- a/tests/github/test_github_script.py +++ b/tests/github/test_github_script.py @@ -15,9 +15,9 @@ def test_script_parse_args_wrong_command(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "argument command: invalid choice: 'rejoice'" in captured.err - ), "Expected help message printed" + assert "argument command: invalid choice: 'rejoice'" in captured.err, ( + "Expected help message printed" + ) def test_script_parse_args_create_command_required_args(monkeypatch, capfd): @@ -29,9 +29,9 @@ def test_script_parse_args_create_command_required_args(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "the following arguments are required: --tag, --commit" in captured.err - ), "Expected help message on missing arguments for 'create' command" + assert "the following arguments are required: --tag, --commit" in captured.err, ( + "Expected help message on missing arguments for 'create' command" + ) def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): @@ -50,7 +50,6 @@ def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): def test_script_create_dry_run(monkeypatch, capfd): - monkeypatch.setattr( sys, "argv", diff --git a/tests/github/test_upload_to_github_release_page.py b/tests/github/test_upload_to_github_release_page.py index 09596c7..5d06473 100644 --- a/tests/github/test_upload_to_github_release_page.py +++ b/tests/github/test_upload_to_github_release_page.py @@ -39,9 +39,9 @@ def test_upload_to_github_release_page_needs_github_token( artifact_for_upload, dry_run=False, ) - assert ( - str(exn.value) == "GITHUB_TOKEN environment variable not set" - ), "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + assert str(exn.value) == "GITHUB_TOKEN environment variable not set", ( + "Expected an exception to be raised on missing GITHUB_TOKEN environment variable" + ) def test_upload_to_github_release_page( @@ -78,9 +78,9 @@ def test_upload_to_github_release_page_unreadable_artifact( artifact_for_upload, dry_run=False, ) - assert any( - "Error reading file" in record.message for record in caplog.records - ), "Expected an error message log entry" + assert any("Error reading file" in record.message for record in caplog.records), ( + "Expected an error message log entry" + ) def test_upload_to_github_release_page_failed( @@ -114,9 +114,9 @@ def test_script_parse_args_wrong_command(monkeypatch, capfd): gh.main() captured = capfd.readouterr() - assert ( - "argument command: invalid choice: 'rejoice'" in captured.err - ), "Expected help message printed" + assert "argument command: invalid choice: 'rejoice'" in captured.err, ( + "Expected help message printed" + ) def test_script_parse_args_upload_command_required_args(monkeypatch, capfd): diff --git a/tests/helper.py b/tests/helper.py index 7d9a82c..ea3ee7f 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,14 +1,17 @@ import shlex import subprocess +from typing import Any, Optional -def spawn_background_process(cmd, stdout=None, stderr=None): +def spawn_background_process( + cmd: str, stdout: Optional[Any] = None, stderr: Optional[Any] = None +) -> subprocess.Popen[Any]: args = shlex.split(cmd) process = subprocess.Popen(args, shell=False, stdout=stdout, stderr=stderr) return process -def call_command(cmd): +def call_command(cmd: str) -> str: try: args = shlex.split(cmd) result = subprocess.run( diff --git a/tests/oci/test_index.py b/tests/oci/test_index.py index 3a4904e..dbdfe08 100644 --- a/tests/oci/test_index.py +++ b/tests/oci/test_index.py @@ -1,11 +1,12 @@ import json +from typing import Any, Dict import pytest from gardenlinux.oci.index import Index -def test_index_init_and_json(): +def test_index_init_and_json() -> None: """Ensure Index init works correctly""" # Arrange idx = Index() @@ -20,7 +21,7 @@ def test_index_init_and_json(): assert decoded == idx -def test_manifests_as_dict(): +def test_manifests_as_dict() -> None: """Verify manifests_as_dict returns correct keys for cname and digest cases.""" # Arrange idx = Index() @@ -36,7 +37,7 @@ def test_manifests_as_dict(): assert result["sha256:def"] == manifest_no_cname -def test_append_manifest_replace(): +def test_append_manifest_replace() -> None: """Ensure append_manifest replaces existing manifest with same cname.""" # Arrange idx = Index() @@ -55,7 +56,7 @@ def test_append_manifest_replace(): assert any(manifest["digest"] == "sha256:new" for manifest in idx["manifests"]) -def test_append_manifest_cname_not_found(): +def test_append_manifest_cname_not_found() -> None: """Test appending new manifest if cname isn't found.""" # Arrange idx = Index() @@ -76,8 +77,8 @@ def test_append_manifest_cname_not_found(): "not-a-dict", {"annotations": {}}, ], -) -def test_append_invalid_input_raises(bad_manifest): +) # type: ignore[misc] +def test_append_invalid_input_raises(bad_manifest: Dict[str, Any]) -> None: """Test proper error handling for invalid append_manifest input.""" # Arrange idx = Index() diff --git a/tests/oci/test_layer.py b/tests/oci/test_layer.py index 15fca53..1d6607a 100644 --- a/tests/oci/test_layer.py +++ b/tests/oci/test_layer.py @@ -27,8 +27,8 @@ def test_dict_property_returns_with_annotations(tmp_path): blob.write_text("data") # Act - l = gl_layer.Layer(blob) - result = l.dict + layer = gl_layer.Layer(blob) + result = layer.dict # Assert assert result["dummy"] is True @@ -41,16 +41,16 @@ def test_getitem_and_delitem_annotations(tmp_path): # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert (__getitem__) - ann = l["annotations"] + ann = layer["annotations"] assert isinstance(ann, dict) assert "org.opencontainers.image.title" in ann # Act / Assert (__delitem__) - l.__delitem__("annotations") - assert l._annotations == {} + layer.__delitem__("annotations") + assert layer._annotations == {} def test_getitem_invalid_key_raises(tmp_path): @@ -58,11 +58,11 @@ def test_getitem_invalid_key_raises(tmp_path): # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert with pytest.raises(KeyError): - _ = l["invalid"] + _ = layer["invalid"] def test_setitem_annotations(tmp_path): @@ -70,35 +70,35 @@ def test_setitem_annotations(tmp_path): # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act new_ann = {"x": "y"} - l.__setitem__("annotations", new_ann) + layer.__setitem__("annotations", new_ann) # Assert - assert l._annotations == new_ann + assert layer._annotations == new_ann def test_setitem_annotations_invalid_raises(tmp_path): # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act / Assert with pytest.raises(KeyError): - _ = l["invalid"] + _ = layer["invalid"] def test_len_iter(tmp_path): # Arrange blob = tmp_path / "blob.txt" blob.write_text("data") - l = gl_layer.Layer(blob) + layer = gl_layer.Layer(blob) # Act - keys = list(iter(l)) + keys = list(iter(layer)) # Assert assert keys == ["annotations"] @@ -109,7 +109,6 @@ def test_gen_metadata_from_file(tmp_path): # Arrange blob = tmp_path / "blob.tar" blob.write_text("data") - l = gl_layer.Layer(blob) # Act arch = "amd64" diff --git a/tests/oci/test_oci.py b/tests/oci/test_oci.py index 3aba1a9..50a2402 100644 --- a/tests/oci/test_oci.py +++ b/tests/oci/test_oci.py @@ -137,9 +137,9 @@ def get_catalog(client): """Get catalog from registry and return repositories list""" catalog_resp = client.do_request(f"{REGISTRY_URL}/v2/_catalog") - assert ( - catalog_resp.status_code == 200 - ), f"Failed to get catalog, status: {catalog_resp.status_code}" + assert catalog_resp.status_code == 200, ( + f"Failed to get catalog, status: {catalog_resp.status_code}" + ) catalog_json = json.loads(catalog_resp.text) return catalog_json.get("repositories", []) @@ -149,9 +149,9 @@ def get_tags(client, repo): """Get tags for a repository""" tags_resp = client.do_request(f"{REGISTRY_URL}/v2/{repo}/tags/list") - assert ( - tags_resp.status_code == 200 - ), f"Failed to get tags for {repo}, status: {tags_resp.status_code}" + assert tags_resp.status_code == 200, ( + f"Failed to get tags for {repo}, status: {tags_resp.status_code}" + ) tags_json = json.loads(tags_resp.text) return tags_json.get("tags", []) @@ -167,9 +167,9 @@ def get_manifest(client, repo, reference): }, ) - assert ( - manifest_resp.status_code == 200 - ), f"Failed to get manifest for {repo}:{reference}, status: {manifest_resp.status_code}" + assert manifest_resp.status_code == 200, ( + f"Failed to get manifest for {repo}:{reference}, status: {manifest_resp.status_code}" + ) # Get the digest and content - use headers.get() instead of header.Get() digest = manifest_resp.headers.get("Docker-Content-Digest") @@ -200,23 +200,23 @@ def verify_combined_tag_manifest(manifest, arch, cname, version, feature_set, co assert "annotations" in manifest, "Manifest should contain annotations" annotations = manifest.get("annotations", {}) - assert ( - annotations.get("architecture") == arch - ), f"Manifest should have architecture {arch}" + assert annotations.get("architecture") == arch, ( + f"Manifest should have architecture {arch}" + ) assert annotations.get("cname") == cname, f"Manifest should have cname {cname}" - assert ( - annotations.get("version") == version - ), f"Manifest should have version {version}" + assert annotations.get("version") == version, ( + f"Manifest should have version {version}" + ) if feature_set: - assert ( - annotations.get("feature_set") == feature_set - ), f"Manifest should have feature_set {feature_set}" + assert annotations.get("feature_set") == feature_set, ( + f"Manifest should have feature_set {feature_set}" + ) if commit: - assert ( - annotations.get("commit") == commit - ), f"Manifest should have commit {commit}" + assert annotations.get("commit") == commit, ( + f"Manifest should have commit {commit}" + ) def verify_additional_tags( @@ -293,7 +293,7 @@ def verify_additional_tags( f"{TEST_VERSION}-patch-{TEST_COMMIT}", f"{TEST_VERSION_STABLE}", f"{TEST_VERSION_STABLE}-stable", - f"latest", + "latest", ], [ f"{TEST_VERSION}-patch-{platform}-{feature_string}-{arch}", diff --git a/tests/s3/conftest.py b/tests/s3/conftest.py index b2a8e64..0626e4d 100644 --- a/tests/s3/conftest.py +++ b/tests/s3/conftest.py @@ -2,12 +2,14 @@ from dataclasses import dataclass from hashlib import md5, sha256 +from io import BytesIO +from pathlib import Path +from typing import Any, Generator import boto3 import pytest from moto import mock_aws -from gardenlinux.features.cname import CName as RealCName BUCKET_NAME = "test-bucket" REGION = "us-east-1" @@ -15,9 +17,9 @@ @dataclass(frozen=True) class S3Env: - s3: object + s3: boto3.client bucket_name: str - tmp_path: str + tmp_path: Path cname: str @@ -34,7 +36,7 @@ def make_cname( # Helpers to compute digests for fake files -def dummy_digest(data: bytes, algo: str) -> str: +def dummy_digest(data: BytesIO, algo: str) -> str: """ Dummy for file_digest() to compute hashes for in-memory byte streams """ @@ -42,15 +44,15 @@ def dummy_digest(data: bytes, algo: str) -> str: data.seek(0) # Reset byte cursor to start for multiple uses if algo == "md5": - return md5(content) # nosec B324 + return md5(content).hexdigest() # nosec B324 elif algo == "sha256": - return sha256(content) + return sha256(content).hexdigest() else: raise ValueError(f"Unsupported algo: {algo}") -@pytest.fixture(autouse=True) -def s3_setup(tmp_path, monkeypatch): +@pytest.fixture(autouse=True) # type: ignore[misc] +def s3_setup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[Any]: """ Provides a clean S3 setup for each test. """ diff --git a/tests/s3/test_bucket.py b/tests/s3/test_bucket.py index 451c10e..433b169 100644 --- a/tests/s3/test_bucket.py +++ b/tests/s3/test_bucket.py @@ -10,11 +10,12 @@ import io from gardenlinux.s3.bucket import Bucket +from .conftest import S3Env REGION = "us-east-1" -def test_bucket_minimal(s3_setup): +def test_bucket_minimal(s3_setup: S3Env) -> None: """ Ensure Bucket initializes correctly. """ @@ -23,7 +24,7 @@ def test_bucket_minimal(s3_setup): assert bucket.name == env.bucket_name -def test_objects_empty(s3_setup): +def test_objects_empty(s3_setup: S3Env) -> None: """ List objects from empty bucket. """ @@ -37,7 +38,7 @@ def test_objects_empty(s3_setup): assert list(bucket.objects) == [] -def test_upload_file_and_list(s3_setup): +def test_upload_file_and_list(s3_setup: S3Env) -> None: """ Create a fake file in a temporary directory, upload and try to list it @@ -58,7 +59,7 @@ def test_upload_file_and_list(s3_setup): assert "example.txt" in all_keys -def test_download_file(s3_setup): +def test_download_file(s3_setup: S3Env) -> None: """ Try to download a file pre-existing in the bucket """ @@ -75,7 +76,7 @@ def test_download_file(s3_setup): assert target_path.read_text() == "some data" -def test_read_cache_file_or_filter(s3_setup): +def test_read_cache_file_or_filter(s3_setup: S3Env) -> None: """ Try to read with cache """ @@ -98,7 +99,7 @@ def test_read_cache_file_or_filter(s3_setup): assert result == ["file.txt", "file2.txt"] -def test_upload_fileobj(s3_setup): +def test_upload_fileobj(s3_setup: S3Env) -> None: """ Upload a file-like in-memory object to the bucket """ @@ -117,7 +118,7 @@ def test_upload_fileobj(s3_setup): assert obj["Body"].read() == b"Test Data" -def test_download_fileobj(s3_setup): +def test_download_fileobj(s3_setup: S3Env) -> None: """ Download data into a in-memory object """ @@ -138,7 +139,7 @@ def test_download_fileobj(s3_setup): assert output.read() == b"123abc" -def test_getattr_delegates(s3_setup): +def test_getattr_delegates(s3_setup: S3Env) -> None: """ Verify that attribute access is delegated to the underlying boto3 Bucket. diff --git a/tests/s3/test_s3_artifacts.py b/tests/s3/test_s3_artifacts.py index d15fe6e..e675887 100644 --- a/tests/s3/test_s3_artifacts.py +++ b/tests/s3/test_s3_artifacts.py @@ -194,7 +194,7 @@ def test_upload_from_directory_succeeds_because_of_release_file(monkeypatch, s3_ """ # Arrange env = s3_setup - (env.tmp_path / f"container.release").write_text(RELEASE_DATA) + (env.tmp_path / "container.release").write_text(RELEASE_DATA) artifacts = S3Artifacts(env.bucket_name) artifacts.upload_from_directory("container", env.tmp_path)