diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3d44d0..1322251 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,5 +53,5 @@ repos: rev: v1.13.0 hooks: - id: mypy - additional_dependencies: ["types-PyYAML"] + additional_dependencies: ["types-PyYAML", "types-requests"] exclude: ^(docs/|.*/test_.*|.*conftest.py) diff --git a/pyproject.toml b/pyproject.toml index 14392d0..130cc9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,8 @@ dependencies = [ "pytest-progress", "python-simple-logger", "pyyaml", + "tenacity", + "types-requests>=2.32.0.20241016", ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py index f131ba3..aeb08d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,8 @@ from ocp_resources.namespace import Namespace from ocp_resources.resource import get_client +from tests.utils import create_ns + @pytest.fixture(scope="session") def admin_client() -> DynamicClient: @@ -11,9 +13,5 @@ def admin_client() -> DynamicClient: @pytest.fixture(scope="class") def model_namespace(request, admin_client: DynamicClient) -> Namespace: - with Namespace( - client=admin_client, - name=request.param["name"], - ) as ns: - ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + with create_ns(client=admin_client, name=request.param["name"]) as ns: yield ns diff --git a/tests/trustyai/conftest.py b/tests/trustyai/conftest.py index 77ea32e..9e69ed1 100644 --- a/tests/trustyai/conftest.py +++ b/tests/trustyai/conftest.py @@ -1,20 +1,111 @@ +import subprocess + import pytest +import yaml from kubernetes.dynamic import DynamicClient +from ocp_resources.config_map import ConfigMap +from ocp_resources.deployment import Deployment from ocp_resources.namespace import Namespace from ocp_resources.pod import Pod from ocp_resources.secret import Secret from ocp_resources.service import Service +from ocp_resources.service_account import ServiceAccount +from ocp_resources.trustyai_service import TrustyAIService +from tests.trustyai.constants import TRUSTYAI_SERVICE, MODELMESH_SERVING +from tests.trustyai.utils import update_configmap_data +from tests.utils import create_ns MINIO: str = "minio" OPENDATAHUB_IO: str = "opendatahub.io" @pytest.fixture(scope="class") -def minio_pod(admin_client: DynamicClient, model_namespace: Namespace) -> Pod: +def trustyai_service_with_pvc_storage( + admin_client: DynamicClient, + ns_with_modelmesh_enabled: Namespace, + modelmesh_serviceaccount: ServiceAccount, + cluster_monitoring_config: ConfigMap, + user_workload_monitoring_config: ConfigMap, +) -> TrustyAIService: + with TrustyAIService( + client=admin_client, + name=TRUSTYAI_SERVICE, + namespace=ns_with_modelmesh_enabled.name, + storage={"format": "PVC", "folder": "/inputs", "size": "1Gi"}, + data={"filename": "data.csv", "format": "CSV"}, + metrics={"schedule": "5s"}, + ) as trustyai_service: + trustyai_deployment = Deployment( + namespace=ns_with_modelmesh_enabled.name, name=TRUSTYAI_SERVICE, wait_for_resource=True + ) + trustyai_deployment.wait_for_replicas() + yield trustyai_service + + +@pytest.fixture(scope="class") +def ns_with_modelmesh_enabled(request, admin_client: DynamicClient): + with create_ns(client=admin_client, name=request.param["name"], labels={"modelmesh-enabled": "true"}) as ns: + yield ns + + +@pytest.fixture(scope="class") +def openshift_token(ns_with_modelmesh_enabled): + return subprocess.check_output(["oc", "whoami", "-t", ns_with_modelmesh_enabled.name]).decode().strip() + + +@pytest.fixture(scope="class") +def modelmesh_serviceaccount(admin_client: DynamicClient, ns_with_modelmesh_enabled: Namespace) -> ServiceAccount: + with ServiceAccount( + client=admin_client, name=f"{MODELMESH_SERVING}-sa", namespace=ns_with_modelmesh_enabled.name + ) as sa: + yield sa + + +@pytest.fixture(scope="session") +def cluster_monitoring_config(admin_client: DynamicClient) -> ConfigMap: + name = "cluster-monitoring-config" + namespace = "openshift-monitoring" + data = {"config.yaml": yaml.dump({"enableUserWorkload": "true"})} + cm = ConfigMap(client=admin_client, name=name, namespace=namespace) + if cm.exists: # This resource is usually created when doing exploratory testing, add this exception for convenience + with update_configmap_data(configmap=cm, data=data) as cm: + yield cm + + with ConfigMap( + client=admin_client, + name=name, + namespace=namespace, + data=data, + ) as cm: + yield cm + + +@pytest.fixture(scope="session") +def user_workload_monitoring_config(admin_client: DynamicClient) -> ConfigMap: + name = "user-workload-monitoring-config" + namespace = "openshift-user-workload-monitoring" + data = {"config.yaml": yaml.dump({"prometheus": {"logLevel": "debug", "retention": "15d"}})} + cm = ConfigMap(client=admin_client, name=name, namespace=namespace) + if cm.exists: # This resource is usually created when doing exploratory testing, add this exception for convenience + with update_configmap_data(configmap=cm, data=data) as cm: + yield cm + + with ConfigMap( + client=admin_client, + name=name, + namespace=namespace, + data=data, + ) as cm: + yield cm + + +@pytest.fixture(scope="class") +def minio_pod(admin_client: DynamicClient, ns_with_modelmesh_enabled: Namespace) -> Pod: with Pod( + client=admin_client, name=MINIO, - namespace=model_namespace.name, + namespace=ns_with_modelmesh_enabled.name, containers=[ { "args": [ @@ -43,10 +134,11 @@ def minio_pod(admin_client: DynamicClient, model_namespace: Namespace) -> Pod: @pytest.fixture(scope="class") -def minio_service(admin_client: DynamicClient, model_namespace: Namespace) -> Service: +def minio_service(admin_client: DynamicClient, ns_with_modelmesh_enabled: Namespace) -> Service: with Service( + client=admin_client, name=MINIO, - namespace=model_namespace.name, + namespace=ns_with_modelmesh_enabled.name, ports=[ { "name": "minio-client-port", @@ -64,11 +156,12 @@ def minio_service(admin_client: DynamicClient, model_namespace: Namespace) -> Se @pytest.fixture(scope="class") def minio_data_connection( - admin_client: DynamicClient, model_namespace: Namespace, minio_pod: Pod, minio_service: Service + admin_client: DynamicClient, ns_with_modelmesh_enabled: Namespace, minio_pod: Pod, minio_service: Service ) -> Secret: with Secret( + client=admin_client, name="aws-connection-minio-data-connection", - namespace=model_namespace.name, + namespace=ns_with_modelmesh_enabled.name, data_dict={ "AWS_ACCESS_KEY_ID": "VEhFQUNDRVNTS0VZ", "AWS_DEFAULT_REGION": "dXMtc291dGg=", diff --git a/tests/trustyai/constants.py b/tests/trustyai/constants.py new file mode 100644 index 0000000..f02ad6d --- /dev/null +++ b/tests/trustyai/constants.py @@ -0,0 +1,5 @@ +TIMEOUT_1MIN = 60 +TIMEOUT_5MIN = 5 * TIMEOUT_1MIN + +TRUSTYAI_SERVICE: str = "trustyai-service" +MODELMESH_SERVING = "modelmesh-serving" diff --git a/tests/trustyai/drift/conftest.py b/tests/trustyai/drift/conftest.py new file mode 100644 index 0000000..67bbc03 --- /dev/null +++ b/tests/trustyai/drift/conftest.py @@ -0,0 +1,98 @@ +import pytest +from kubernetes.dynamic import DynamicClient +from ocp_resources.deployment import Deployment +from ocp_resources.inference_service import InferenceService +from ocp_resources.namespace import Namespace +from ocp_resources.secret import Secret +from ocp_resources.serving_runtime import ServingRuntime +from ocp_resources.trustyai_service import TrustyAIService + +from tests.trustyai.constants import MODELMESH_SERVING +from tests.trustyai.drift.utils import wait_for_modelmesh_pods_registered_by_trustyai + +MLSERVER: str = "mlserver" +MLSERVER_RUNTIME_NAME: str = f"{MLSERVER}-1.x" +MLSERVER_QUAY_IMAGE: str = "quay.io/aaguirre/mlserver@sha256:8884d989b3063a47bf0e6c20c1c0ff253662121a977fe5b74b54e682839360d4" # TODO: Move this image to a better place +XGBOOST = "xgboost" +SKLEARN = "sklearn" + + +@pytest.fixture(scope="class") +def mlserver_runtime( + admin_client: DynamicClient, minio_data_connection: Secret, ns_with_modelmesh_enabled: Namespace +) -> ServingRuntime: + supported_model_formats = [ + {"name": SKLEARN, "version": "0", "autoselect": "true"}, + {"name": XGBOOST, "version": "1", "autoselect": "true"}, + {"name": "lightgbm", "version": "3", "autoselect": "true"}, + ] + containers = [ + { + "name": MLSERVER, + "image": MLSERVER_QUAY_IMAGE, + "env": [ + {"name": "MLSERVER_MODELS_DIR", "value": "/models/_mlserver_models/"}, + {"name": "MLSERVER_GRPC_PORT", "value": "8001"}, + {"name": "MLSERVER_HTTP_PORT", "value": "8002"}, + {"name": "MLSERVER_LOAD_MODELS_AT_STARTUP", "value": "false"}, + {"name": "MLSERVER_MODEL_NAME", "value": "dummy-model-fixme"}, + {"name": "MLSERVER_HOST", "value": "127.0.0.1"}, + {"name": "MLSERVER_GRPC_MAX_MESSAGE_LENGTH", "value": "-1"}, + ], + "resources": {"requests": {"cpu": "500m", "memory": "1Gi"}, "limits": {"cpu": "5", "memory": "1Gi"}}, + } + ] + + with ServingRuntime( + client=admin_client, + name=MLSERVER_RUNTIME_NAME, + namespace=ns_with_modelmesh_enabled.name, + containers=containers, + supported_model_formats=supported_model_formats, + multi_model=True, + protocol_versions=["grpc-v2"], + grpc_endpoint="port:8085", + grpc_data_endpoint="port:8001", + built_in_adapter={ + "serverType": MLSERVER, + "runtimeManagementPort": 8001, + "memBufferBytes": 134217728, + "modelLoadingTimeoutMillis": 90000, + }, + annotations={"enable-route": "true"}, + label={"name": f"{MODELMESH_SERVING}-{MLSERVER_RUNTIME_NAME}-SR"}, + ) as mlserver: + yield mlserver + + +@pytest.fixture(scope="class") +def gaussian_credit_model( + admin_client: DynamicClient, + ns_with_modelmesh_enabled: Namespace, + minio_data_connection: Secret, + mlserver_runtime: ServingRuntime, + trustyai_service_with_pvc_storage: TrustyAIService, +) -> InferenceService: + name = "gaussian-credit-model" + with InferenceService( + client=admin_client, + name=name, + namespace=ns_with_modelmesh_enabled.name, + predictor={ + "model": { + "modelFormat": {"name": XGBOOST}, + "runtime": mlserver_runtime.name, + "storage": {"key": minio_data_connection.name, "path": f"{SKLEARN}/{name.replace('-', '_')}.json"}, + } + }, + annotations={f"{InferenceService.ApiGroup.SERVING_KSERVE_IO}/deploymentMode": "ModelMesh"}, + ) as inference_service: + deployment = Deployment( + client=admin_client, + namespace=ns_with_modelmesh_enabled.name, + name=f"{MODELMESH_SERVING}-{mlserver_runtime.name}", + wait_for_resource=True, + ) + deployment.wait_for_replicas() + wait_for_modelmesh_pods_registered_by_trustyai(client=admin_client, namespace=ns_with_modelmesh_enabled.name) + yield inference_service diff --git a/tests/trustyai/drift/model_data/data_batches/0.json b/tests/trustyai/drift/model_data/data_batches/0.json new file mode 100644 index 0000000..5e7d6a3 --- /dev/null +++ b/tests/trustyai/drift/model_data/data_batches/0.json @@ -0,0 +1,41 @@ +{ + "inputs": [ + { + "name": "credit_inputs", + "shape": [5, 4], + "datatype": "FP64", + "data": [ + [ + 50.76899726547295, + 491.57388556551217, + 11.464223840747968, + 28.29901088554935 + ], + [ + 47.80397343448882, + 515.4934209257955, + 13.060710933476372, + 23.710220802886678 + ], + [ + 56.731646594370886, + 441.00792486531225, + 10.616678496549381, + 19.040822238191925 + ], + [ + 38.35835814888362, + 540.3557103621482, + 11.206492946288046, + 15.561855179575819 + ], + [ + 47.422935471130046, + 529.5980108579066, + 10.625654599760802, + 16.264208531245814 + ] + ] + } + ] +} diff --git a/tests/trustyai/drift/model_data/data_batches/5.json b/tests/trustyai/drift/model_data/data_batches/5.json new file mode 100644 index 0000000..11973f6 --- /dev/null +++ b/tests/trustyai/drift/model_data/data_batches/5.json @@ -0,0 +1,44 @@ +{ + "inputs": [ + { + "name": "credit_inputs", + "shape": [ + 5, + 4 + ], + "datatype": "FP64", + "data": [ + [ + 47.10141440274886, + 511.93953118897673, + 10.30958871700256, + 28.462273005138734 + ], + [ + 55.018917461242545, + 529.5655000531016, + 10.657507738326363, + 20.254038773880144 + ], + [ + 43.36735715813185, + 459.45376201509356, + 11.974670802162198, + 16.815021767153233 + ], + [ + 48.20533085890888, + 484.26866990656447, + 9.765379302729444, + 20.95457742333733 + ], + [ + 44.52555863555142, + 448.30189185655723, + 12.468831395634185, + 30.501275682394212 + ] + ] + } + ] +} diff --git a/tests/trustyai/drift/test_drift.py b/tests/trustyai/drift/test_drift.py new file mode 100644 index 0000000..7d6e2c9 --- /dev/null +++ b/tests/trustyai/drift/test_drift.py @@ -0,0 +1,38 @@ +import pytest + +from tests.trustyai.drift.utils import send_inference_requests_and_verify_trustyai_service + + +@pytest.mark.parametrize( + "ns_with_modelmesh_enabled", + [ + pytest.param( + {"name": "test-drift-gaussian-credit-model"}, + ) + ], + indirect=True, +) +class TestDriftMetrics: + """ + Verifies all the basic operations with a drift metric (meanshift) available in TrustyAI, using PVC storage. + + 1. Send data to the model (gaussian_credit_model) and verify that TrustyAI registers the observations. + """ + + def test_send_inference_request_and_verify_trustyai_service( + self, + admin_client, + openshift_token, + ns_with_modelmesh_enabled, + trustyai_service_with_pvc_storage, + gaussian_credit_model, + ) -> None: + send_inference_requests_and_verify_trustyai_service( + client=admin_client, + token=openshift_token, + data_path="./tests/trustyai/drift/model_data/data_batches", + trustyai_service=trustyai_service_with_pvc_storage, + inference_service=gaussian_credit_model, + ) + + # TODO: Add rest of operations in upcoming PRs (upload data directly to Trusty, send metric request, schedule period metric calculation, delete metric request). diff --git a/tests/trustyai/drift/utils.py b/tests/trustyai/drift/utils.py new file mode 100644 index 0000000..2051bf3 --- /dev/null +++ b/tests/trustyai/drift/utils.py @@ -0,0 +1,244 @@ +import json +import os +import subprocess +from typing import Any, Dict, List, Optional + +import requests +from kubernetes.dynamic import DynamicClient +from kubernetes.dynamic.exceptions import NotFoundError +from ocp_resources.inference_service import InferenceService +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod +from ocp_resources.route import Route +from ocp_resources.trustyai_service import TrustyAIService +from simple_logger.logger import get_logger +from timeout_sampler import TimeoutSampler + +from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type + +from tests.trustyai.constants import TIMEOUT_5MIN, MODELMESH_SERVING + +LOGGER = get_logger(name=__name__) +TIMEOUT_30SEC: int = 30 + + +class TrustyAIServiceRequestHandler: + """ + Class to encapsulate the behaviors associated to the different TrustyAIService requests we make in the tests + TODO: It will be moved to a more general file when we start using it in new tests. + """ + + def __init__(self, token: str, service: TrustyAIService, client: DynamicClient): + self.token = token + self.service = service + self.headers = {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"} + self.service_route = Route( + client=client, namespace=service.namespace, name="trustyai-service", ensure_exists=True + ) + + def _send_request( + self, + endpoint: str, + method: str, + data: Optional[Dict[str, Any]] = None, + json: Optional[Dict[str, Any]] = None, + ) -> Any: + url = f"https://{self.service_route.host}{endpoint}" + + if method not in ("GET", "POST"): + raise ValueError(f"Unsupported HTTP method: {method}") + if method == "GET": + return requests.get(url=url, headers=self.headers, verify=False) + elif method == "POST": + return requests.post(url=url, headers=self.headers, data=data, json=json, verify=False) + + def get_model_metadata(self) -> Any: + return self._send_request(endpoint="/info", method="GET") + + +def create_ocp_token(namespace: Namespace) -> str: + return subprocess.check_output(["oc", "create", "token", "test-user", "-n", namespace.name]).decode().strip() + + +def send_inference_request( + token: str, + inference_route: Route, + data_batch: Any, + file_path: str, + max_retries: int = 5, +) -> None: + """ + Send data batch to inference service with retry logic for network errors. + + Args: + token: Authentication token + inference_route: Route of the inference service + data_batch: Data to be sent + file_path: Path to the file being processed + max_retries: Maximum number of retry attempts (default: 5) + + Returns: + None + + Raises: + RequestException: If all retry attempts fail + """ + url: str = f"https://{inference_route.host}{inference_route.instance.spec.path}/infer" + headers: Dict[str, str] = {"Authorization": f"Bearer {token}"} + + @retry( + stop=stop_after_attempt(max_retries), + wait=wait_exponential(multiplier=1, min=4, max=10), + retry=retry_if_exception_type(requests.RequestException), + before_sleep=lambda retry_state: LOGGER.warning( + f"Retry attempt {retry_state.attempt_number} for file {file_path} after error. " + f"Waiting {retry_state.next_action.sleep} seconds..." + ), + ) + def _make_request() -> None: + try: + response: requests.Response = requests.post( + url=url, headers=headers, data=data_batch, verify=False, timeout=TIMEOUT_30SEC + ) + response.raise_for_status() + except requests.RequestException as e: + LOGGER.error(response.content) + LOGGER.error(f"Error sending data for file: {file_path}. Error: {str(e)}") + raise + + try: + _make_request() + except requests.RequestException: + LOGGER.error(f"All {max_retries} retry attempts failed for file: {file_path}") + raise + + +def get_trustyai_number_of_observations(client: DynamicClient, token: str, trustyai_service: TrustyAIService) -> int: + handler = TrustyAIServiceRequestHandler(token=token, service=trustyai_service, client=client) + model_metadata: requests.Response = handler.get_model_metadata() + + if not model_metadata: + return 0 + + try: + metadata_json: Any = model_metadata.json() + + if not metadata_json: + return 0 + + model_key: str = next(iter(metadata_json)) + model = metadata_json.get(model_key) + if not model: + raise KeyError(f"Model data not found for key: {model_key}") + + if observations := model.get("data", {}).get("observations"): + return observations + + raise KeyError("Observations data not found in model metadata") + except Exception as e: + raise TypeError(f"Failed to parse response: {str(e)}") + + +def wait_for_trustyai_to_register_inference_request( + client: DynamicClient, token: str, trustyai_service: TrustyAIService, expected_observations: int +) -> None: + current_observations: int = get_trustyai_number_of_observations( + client=client, token=token, trustyai_service=trustyai_service + ) + + samples = TimeoutSampler( + wait_timeout=TIMEOUT_30SEC, + sleep=1, + func=lambda: current_observations == expected_observations, + ) + for sample in samples: + if sample: + return + + +def send_inference_requests_and_verify_trustyai_service( + client: DynamicClient, + token: str, + data_path: str, + trustyai_service: TrustyAIService, + inference_service: InferenceService, +) -> None: + """ + Sends all the data batches present in a given directory to an InferenceService, and verifies that TrustyAIService has registered the observations. + + Args: + client (DynamicClient): The client instance for making API calls. + token (str): Authentication token for API access. + data_path (str): Directory path containing data batch files. + trustyai_service (TrustyAIService): TrustyAIService that will register the model. + inference_service (InferenceService): Model to be registered by TrustyAI. + """ + inference_route: Route = Route(client=client, namespace=inference_service.namespace, name=inference_service.name) + + for root, _, files in os.walk(data_path): + for file_name in files: + file_path = os.path.join(root, file_name) + + with open(file_path, "r") as file: + data = file.read() + + current_observations = get_trustyai_number_of_observations( + client=client, token=token, trustyai_service=trustyai_service + ) + send_inference_request(token=token, inference_route=inference_route, data_batch=data, file_path=file_path) + wait_for_trustyai_to_register_inference_request( + client=client, + token=token, + trustyai_service=trustyai_service, + expected_observations=current_observations + json.loads(data)["inputs"][0]["shape"][0], + ) + + +def wait_for_modelmesh_pods_registered_by_trustyai(client: DynamicClient, namespace: Namespace) -> None: + """ + Check if all the ModelMesh pods in a given namespace are ready and have been registered by the TrustyAIService in that same namespace. + + Args: + client (DynamicClient): The client instance for interacting with the cluster. + namespace (Namespace): The namespace where ModelMesh pods and TrustyAIService are deployed. + """ + + def _check_pods_ready_with_env() -> bool: + modelmesh_pods: List[Pod] = [ + pod + for pod in Pod.get(client=client, namespace=namespace) + if pod.labels.get("modelmesh-service") == MODELMESH_SERVING + ] + + found_pod_with_env: bool = False + + for pod in modelmesh_pods: + try: + has_env_var = False + # Check containers for environment variable + for container in pod.instance.spec.containers: + if container.env is not None and any(env.name == "MM_PAYLOAD_PROCESSORS" for env in container.env): + has_env_var = True + found_pod_with_env = True + break + + # If pod has env var but isn't running, return False + if has_env_var and pod.status != Pod.Status.RUNNING: + return False + + except NotFoundError: + # Ignore pods that were deleted during the process + continue + + # Return True only if we found at least one pod with the env var + # and all pods with the env var are running + return found_pod_with_env + + samples = TimeoutSampler( + wait_timeout=TIMEOUT_5MIN, + sleep=TIMEOUT_30SEC, + func=_check_pods_ready_with_env, + ) + for sample in samples: + if sample: + return diff --git a/tests/trustyai/utils.py b/tests/trustyai/utils.py new file mode 100644 index 0000000..aa56bc5 --- /dev/null +++ b/tests/trustyai/utils.py @@ -0,0 +1,14 @@ +from contextlib import contextmanager +from typing import Dict, Any + +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import ResourceEditor + + +@contextmanager +def update_configmap_data(configmap: ConfigMap, data: Dict[str, Any]) -> ResourceEditor: + if configmap.data == data: + yield configmap + else: + with ResourceEditor(patches={configmap: {"data": data}}) as update: + yield update diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..43729de --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,23 @@ +from contextlib import contextmanager +from typing import Any, Dict, Optional + +from kubernetes.dynamic import DynamicClient +from ocp_resources.namespace import Namespace + +TIMEOUT_2MIN = 2 * 60 + + +@contextmanager +def create_ns( + client: DynamicClient, + name: str, + labels: Optional[Dict[str, Any]] = None, + wait_for_resource: bool = True, +) -> Namespace: + with Namespace( + client=client, + name=name, + label=labels, + wait_for_resource=wait_for_resource, + ) as ns: + yield ns diff --git a/uv.lock b/uv.lock index a868c8d..ab2d956 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,15 @@ resolution-markers = [ "python_full_version >= '3.11'", ] +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + [[package]] name = "asttokens" version = "2.4.1" @@ -17,6 +26,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, ] +[[package]] +name = "backcall" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/40/764a663805d84deee23043e1426a9175567db89c8b3287b5c2ad9f71aa93/backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", size = 18041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/1c/ff6546b6c12603d8dd1070aa3c3d273ad4c07f5771689a7b69a550e8c951/backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255", size = 11157 }, +] + [[package]] name = "bcrypt" version = "4.2.0" @@ -136,6 +154,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, + { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, + { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, + { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, + { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, + { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, @@ -286,34 +312,34 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, - { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, - { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, - { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, - { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, - { url = "https://files.pythonhosted.org/packages/6f/db/d8b8a039483f25fc3b70c90bc8f3e1d4497a99358d610c5067bf3bd4f0af/cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", size = 3144545 }, - { url = "https://files.pythonhosted.org/packages/93/90/116edd5f8ec23b2dc879f7a42443e073cdad22950d3c8ee834e3b8124543/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", size = 3679828 }, - { url = "https://files.pythonhosted.org/packages/d8/32/1e1d78b316aa22c0ba6493cc271c1c309969e5aa5c22c830a1d7ce3471e6/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", size = 3908132 }, - { url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811 }, - { url = "https://files.pythonhosted.org/packages/cc/fc/ff7c76afdc4f5933b5e99092528d4783d3d1b131960fc8b31eb38e076ca8/cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", size = 3146844 }, - { url = "https://files.pythonhosted.org/packages/d7/29/a233efb3e98b13d9175dcb3c3146988ec990896c8fa07e8467cce27d5a80/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", size = 3681997 }, - { url = "https://files.pythonhosted.org/packages/c0/cf/c9eea7791b961f279fb6db86c3355cfad29a73141f46427af71852b23b95/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", size = 3905208 }, - { url = "https://files.pythonhosted.org/packages/21/ea/6c38ca546d5b6dab3874c2b8fc6b1739baac29bacdea31a8c6c0513b3cfa/cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", size = 2989787 }, +sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222 }, + { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751 }, + { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827 }, + { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034 }, + { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407 }, + { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457 }, + { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499 }, + { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504 }, + { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456 }, + { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263 }, + { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368 }, + { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750 }, + { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925 }, + { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152 }, + { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392 }, + { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606 }, + { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948 }, + { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445 }, + { url = "https://files.pythonhosted.org/packages/18/23/4175dcd935e1649865e1af7bd0b827cc9d9769a586dcc84f7cbe96839086/cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", size = 3152694 }, + { url = "https://files.pythonhosted.org/packages/ea/45/967da50269954b993d4484bf85026c7377bd551651ebdabba94905972556/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", size = 3713077 }, + { url = "https://files.pythonhosted.org/packages/df/e6/ccd29a1f9a6b71294e1e9f530c4d779d5dd37c8bb736c05d5fb6d98a971b/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289", size = 3915597 }, + { url = "https://files.pythonhosted.org/packages/a2/80/fb7d668f1be5e4443b7ac191f68390be24f7c2ebd36011741f62c7645eb2/cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", size = 2989208 }, + { url = "https://files.pythonhosted.org/packages/b2/aa/782e42ccf854943dfce72fb94a8d62220f22084ff07076a638bc3f34f3cc/cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", size = 3154685 }, + { url = "https://files.pythonhosted.org/packages/3e/fd/70f3e849ad4d6cca2118ee6938e0b52326d02406f10912356151dd4b6868/cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", size = 3713909 }, + { url = "https://files.pythonhosted.org/packages/21/b0/4ecefa99519eaa32af49a3ad002bb3e795f9e6eb32221fd87736247fa3cb/cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", size = 3916544 }, + { url = "https://files.pythonhosted.org/packages/8c/42/2948dd87b237565c77b28b674d972c7f983ffa3977dc8b8ad0736f6a7d97/cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", size = 2989774 }, ] [[package]] @@ -427,12 +453,15 @@ name = "ipython" version = "8.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "backcall" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "decorator" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "jedi" }, { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "pickleshare" }, { name = "prompt-toolkit" }, { name = "pygments" }, { name = "stack-data" }, @@ -619,6 +648,8 @@ dependencies = [ { name = "pytest-progress" }, { name = "python-simple-logger" }, { name = "pyyaml" }, + { name = "tenacity" }, + { name = "types-requests" }, ] [package.dev-dependencies] @@ -634,6 +665,8 @@ requires-dist = [ { name = "pytest-progress" }, { name = "python-simple-logger" }, { name = "pyyaml" }, + { name = "tenacity" }, + { name = "types-requests", specifier = ">=2.32.0.20241016" }, ] [package.metadata.requires-dev] @@ -758,6 +791,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] +[[package]] +name = "pickleshare" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", size = 6161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -838,7 +880,7 @@ wheels = [ [[package]] name = "pyhelper-utils" -version = "1.0.1" +version = "0.0.44" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipdb" }, @@ -847,7 +889,7 @@ dependencies = [ { name = "requests" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/86/ec115d7388a0742b7abc8add7f3661e07759ea9ce220bd95f064a9c9c861/pyhelper_utils-1.0.1.tar.gz", hash = "sha256:beae9c5deaa0e4cbca757c7b5371d08ec5a5fd1af20a8b9dfb9acfbdb9cf0b04", size = 9753 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/ef/384a05ec3d49a6f7cdceedf012dba834bb760087c5e5ee7cfcd30e3ef642/pyhelper_utils-0.0.44.tar.gz", hash = "sha256:de21bc9de7eea02bc4e1b85686e18eb49dde3d7f41b45f1cfc498df0e47abc17", size = 9756 } [[package]] name = "pynacl" @@ -1018,6 +1060,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, @@ -1149,6 +1198,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] +[[package]] +name = "tenacity" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, +] + [[package]] name = "text-unidecode" version = "1.3" @@ -1185,6 +1243,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"