diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5178e8f..d92b124 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,10 +15,10 @@ jobs: python-version: '3.9' - name: Install pipenv - run: pip install pipenv + run: pip install poetry - name: Install test dependencies - run: pipenv sync --dev + run: poetry install - name: Run Pytest - run: pipenv run pytest --cov-report term-missing --cov=src --cov-config=./tests/coverage.rc ./tests/ \ No newline at end of file + run: poetry run pytest --cov-report term-missing --cov=src --cov-config=./tests/coverage.rc ./tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac145ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.coverage +.venv +.DS_Store +*.pyc +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0f6f0f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "python.testing.pytestArgs": [ + "tests", + "dpytools" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test_*.py" + ], +} \ No newline at end of file diff --git a/Makefile b/Makefile index 94fc78a..c01abb7 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,12 @@ help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -fmt: ## (Format) - runs black and isort against the codebase (auto triggered on pre-commit) - pipenv run black ./src/* - pipenv run isort ./src/* +fmt: ## (Format) - runs black and isort against the codebase + poetry run black ./dpytools/* + poetry run isort ./dpytools/* -lint: ## Run the ruff python linter (auto triggered on pre-commit) - pipenv run ruff ./src/* +lint: ## Run the ruff python linter + poetry run ruff ./dpytools/* -test: ## Run pytest and check test coverage (auto triggered on pre-push) - pipenv run pytest --cov-report term-missing --cov=src --cov-config=./tests/coverage.rc +test: ## Run pytest and check test coverage + poetry run pytest --cov-report term-missing --cov=dpytools diff --git a/README.md b/README.md index 8090ac5..902bc87 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,34 @@ Simple reusable python resources for digital publishing. ## Installation -**TODO** +Follow these steps to install the package on your local machine: + +1. **Install the package** + + Open your terminal and run the following command: + + ```bash + pip install git+https://github.com/GSS-Cogs/dp-python-tools.git + ``` + ## Usage The following clients, helpers etc are availible upon installation of this package. -| Name | Import | Description | -| ----- | ---------------- | ------ | -| [config](./dpytools/config/README.md) | `from dpytools import Config` | A simple validating configuration class | +| Name | Description | +| ----- | ---------------- | +| [Config](./dpytools/config/README.md) | A simple validating configuration class | **TODO** - all helpers and clients to appear in this list. The usage instructions please see the readme in the appropriate sub directory, this can also be access by clicking the link in the "name" column above. ## Development -All commits that make it to PR should have black and ruff already ran against them, you can do this via `make fmt`. -` +All commits that make it to PR should have black and ruff already ran against them, you can do this via `make fmt` and you can lint via `make lint`. + +For a full list of functionality provided by the makefile just run a naked `make`. ### Repository Organization -Each client, helper etc should be a sub directory of `./dpytools`. Separation between these sub codebaes should be maintained as much as possible aginst the day where we want to break some or all of these tools out into separate repositories. \ No newline at end of file +Each client, helper etc should be a sub directory of `./dpytools`. Separation between these sub codebases should be maintained as much as possible aginst the day where we want to break some or all of these tools out into separate repositories. \ No newline at end of file diff --git a/dpytools/__init__.py b/dpytools/__init__.py index bff3af3..e69de29 100644 --- a/dpytools/__init__.py +++ b/dpytools/__init__.py @@ -1,5 +0,0 @@ -from http.http import HttpClient -from config.config import Config -from logger.logger import logger -from slack.slack import SlackNotifier -from sns.sns import Subscription, publish \ No newline at end of file diff --git a/dpytools/config/config.py b/dpytools/config/config.py index c8b8daf..4dda9a7 100644 --- a/dpytools/config/config.py +++ b/dpytools/config/config.py @@ -1,39 +1,87 @@ -from typing import Dict +from __future__ import annotations -from properties.base import BaseProperty +import os +from typing import Any, Dict, List + +from .properties.base import BaseProperty +from .properties.intproperty import IntegerProperty +from .properties.string import StringProperty class Config: + def __init__(self): + self._properties_to_validate: List[BaseProperty] = [] @staticmethod - def from_env(config_dict: Dict[str, BaseProperty]): - # TODO = read in and populate property classes as - # per the example in the main readme. - # You need to populate with dot notation in mind so: - # - # StringProperty("fieldname", "fieldvalue") - # - # should be accessed on Config/self, so: - # - # value = config.fieldvalue.value - # i.e - # config.fieldvalue = StringProperty("fieldname", "fieldvalue") - # - # Worth looking at the __setattr_ dunder method and a loop - # for how to do this. - # - # Do track the BaseProperty's that you add ready for - # assert_valid_config call. - ... + def from_env(config_dict: Dict[str, Dict[str, Any]]) -> Config: + config = Config() + + for env_var_name, value in config_dict.items(): + value_for_property = os.environ.get(env_var_name, None) + assert ( + value_for_property is not None + ), f'Required envionrment value "{env_var_name}" could not be found.' + + if value["class"] == StringProperty: + if value["kwargs"]: + regex = value["kwargs"].get("regex") + min_len = value["kwargs"].get("min_len") + max_len = value["kwargs"].get("max_len") + else: + regex = None + min_len = None + max_len = None + + stringprop = StringProperty( + _name=value["property"], + _value=value_for_property, + regex=regex, + min_len=min_len, + max_len=max_len, + ) + + prop_name = value["property"] + setattr(config, prop_name, stringprop) + config._properties_to_validate.append(stringprop) + + elif value["class"] == IntegerProperty: + if value["kwargs"]: + min_val = value["kwargs"].get("min_val") + max_val = value["kwargs"].get("max_val") + else: + min_val = None + max_val = None + + intprop = IntegerProperty( + _name=value["property"], + _value=value_for_property, + min_val=min_val, + max_val=max_val, + ) + + prop_name = value["property"] + setattr(config, prop_name, intprop) + config._properties_to_validate.append(intprop) + + else: + prop_type = value["class"] + raise TypeError( + f"Unsupported property type specified via 'property' field, got {prop_type}. Should be of type StringProperty or IntegerProperty" + ) + + return config def assert_valid_config(self): """ Assert that then Config class has the properties that provided properties. """ + for property in self._properties_to_validate: + property.type_is_valid() + property.secondary_validation() + + self._properties_to_validate = [] # For each of the properties you imbided above, run # self.type_is_valid() # self.secondary_validation() - - diff --git a/dpytools/config/properties/__init__.py b/dpytools/config/properties/__init__.py index c7e4bfb..26d2776 100644 --- a/dpytools/config/properties/__init__.py +++ b/dpytools/config/properties/__init__.py @@ -1 +1,2 @@ -from string import StringProperty \ No newline at end of file +from .intproperty import IntegerProperty +from .string import StringProperty diff --git a/dpytools/config/properties/base.py b/dpytools/config/properties/base.py index 7a1dd4d..11a6195 100644 --- a/dpytools/config/properties/base.py +++ b/dpytools/config/properties/base.py @@ -1,26 +1,38 @@ from abc import ABCMeta, abstractmethod from dataclasses import dataclass -from typing import Any, Union, Tuple, Optional +from typing import Any + @dataclass -class BaseProperty(meta=ABCMeta): - name: str - value: Any +class BaseProperty(metaclass=ABCMeta): + _name: str + _value: Any + + @property + def name(self): + return self._name - # TODO: getter - # allow someone to get the property + @name.setter + def name(self, value): + raise ValueError( + f"Trying to change name property to value {value} but you cannot change a property name after instantiation." + ) - # TODO: setter - # categorically disallow anyone from - # changing a property after the class - # has been instantiated. - # Refuse to do it, and log an error. + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + raise ValueError( + f"Trying to change value to {value} but you cannot change a property value after instantiation." + ) @abstractmethod def type_is_valid(self): """ Validate that the property looks like - its of the correct type + its of the correct type """ ... @@ -32,4 +44,4 @@ def secondary_validation(self): Non type based validation you might want to run against a configuration value. """ - ... \ No newline at end of file + ... diff --git a/dpytools/config/properties/intproperty.py b/dpytools/config/properties/intproperty.py new file mode 100644 index 0000000..4a5ff81 --- /dev/null +++ b/dpytools/config/properties/intproperty.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import Optional + +from .base import BaseProperty + + +@dataclass +class IntegerProperty(BaseProperty): + min_val: Optional[int] + max_val: Optional[int] + + def type_is_valid(self): + """ + Validate that the property looks like + its of the correct type + """ + try: + int(self._value) + except Exception as err: + raise Exception( + f"Cannot cast {self._name} value {self._value} to integer." + ) from err + + def secondary_validation(self): + """ + Non type based validation you might want to + run against a configuration value of this kind. + """ + if not self._value: + raise ValueError(f"Integer value for {self._name} does not exist.") + + if self.min_val and self._value < self.min_val: + raise ValueError( + f"Integer value for {self._name} is lower than allowed minimum." + ) + + if self.max_val and self._value > self.max_val: + raise ValueError( + f"Integer value for {self._name} is higher than allowed maximum." + ) diff --git a/dpytools/config/properties/string.py b/dpytools/config/properties/string.py index a072552..cfb31c1 100644 --- a/dpytools/config/properties/string.py +++ b/dpytools/config/properties/string.py @@ -1,39 +1,53 @@ +import re +from dataclasses import dataclass from typing import Optional -from base import BaseProperty +from .base import BaseProperty +@dataclass class StringProperty(BaseProperty): regex: Optional[str] min_len: Optional[int] max_len: Optional[int] - def type_is_valid(self) -> Optional[Exception]: + def type_is_valid(self): """ Validate that the property looks like - its of the correct type + its of the correct type """ try: - str(self.value) + str(self._value) except Exception as err: - raise Exception(f"Cannot cast {self.name} value {self.value} to string.") from err + raise Exception( + f"Cannot cast {self.name} value {self._value} to string." + ) from err - def secondary_validation_passed(self) -> Optional[Exception]: + def secondary_validation(self): """ Non type based validation you might want to run against a configuration value of this kind. """ - if len(self.value) == 0: + + if len(self._value) == 0: raise ValueError(f"Str value for {self.name} is an empty string") - + if self.regex: # TODO - confirm the value matches the regex - ... + regex_search = re.search(self.regex, self._value) + if not regex_search: + raise ValueError( + f"Str value for {self.name} does not match the given regex." + ) if self.min_len: - # TODO - confirm the string matches of exceeds the minimum length - ... + if len(self._value) < self.min_len: + raise ValueError( + f"Str value for {self.name} is shorter than minimum length {self.min_len}" + ) if self.max_len: - # TODO - confirm the value matches or is less than the max length - ... \ No newline at end of file + if len(self._value) > self.max_len: + raise ValueError( + f"Str value for {self.name} is longer than maximum length {self.max_len}" + ) diff --git a/dpytools/http/http.py b/dpytools/http/http.py deleted file mode 100644 index 0abe2c7..0000000 --- a/dpytools/http/http.py +++ /dev/null @@ -1,18 +0,0 @@ - -# Import and use backoff -# https://pypi.org/project/backoff/ - -class HttpClient: - - # Methods should use backoff - # Methods should lof what they are trying to do before they do it. - # When you backoff and retry it should log each failure and why, - # as well as which attempt number it is. - - def get(): - ... - - def post(): - ... - - \ No newline at end of file diff --git a/dpytools/http/README.md b/dpytools/http_clients/README.md similarity index 100% rename from dpytools/http/README.md rename to dpytools/http_clients/README.md diff --git a/dpytools/http_clients/base.py b/dpytools/http_clients/base.py new file mode 100644 index 0000000..ab6458d --- /dev/null +++ b/dpytools/http_clients/base.py @@ -0,0 +1,87 @@ +import logging + +import backoff +import requests +from requests.exceptions import HTTPError + + +# Function to log retry attempts +def log_retry(details): + logging.error(f"Request failed, retrying... Attempt #{details['tries']}") + + +class BaseHttpClient: + # Initialize HttpClient with a backoff_max value + def __init__(self, backoff_max=30): + self.backoff_max = backoff_max + + # GET request method with exponential backoff + @backoff.on_exception(backoff.expo, HTTPError, max_time=30, on_backoff=log_retry) + def get(self, url, *args, **kwargs): + """ + Sends a GET request to the specified URL with optional extra arguments. + + This method is a thin wrapper around `requests.get()`. Any additional arguments + are passed directly to `requests.get()`. For more information on the available + arguments, refer to the `requests.get()` documentation: + https://docs.python-requests.org/en/latest/api/#requests.get + + Args: + url (str): The URL to send the GET request to. + *args: Optional positional arguments passed to `requests.get()`. + **kwargs: Optional keyword arguments passed to `requests.get()`. + + Returns: + Response: The Response object from `requests.get()`. + Raises: + HTTPError: If the request fails for a network-related reason. + """ + return self._handle_request("GET", url, *args, **kwargs) + + # POST request method with exponential backoff + @backoff.on_exception( + backoff.expo, + HTTPError, + max_time=30, + on_backoff=log_retry, + ) + def post(self, url, *args, **kwargs): + """ + Sends a POST request to the specified URL with optional extra arguments. + + This method is a thin wrapper around `requests.post()`. Any additional arguments + are passed directly to `requests.post()`. For more information on the available + arguments, refer to the `requests.post()` documentation: + https://docs.python-requests.org/en/latest/api/#requests.post + + Args: + url (str): The URL to send the POST request to. + *args: Optional positional arguments passed to `requests.post()`. + **kwargs: Optional keyword arguments passed to `requests.post()`. + + Returns: + Response: The Response object from `requests.post()`. + + Raises: + HTTPError: If the request fails for a network-related reason. + """ + return self._handle_request("POST", url, *args, **kwargs) + + # Method to handle requests for GET and POST + def _handle_request(self, method, url, *args, **kwargs): + logging.info(f"Sending {method} request to {url}") + try: + response = requests.request(method, url, *args, **kwargs) + response.raise_for_status() + return response + + except HTTPError as http_err: + logging.error( + f"HTTP error occurred: {http_err} when sending a {method} to {url} with headers {kwargs.get('headers')}" + ) + raise http_err + except Exception as err: + logging.error( + f"Other error occurred: {err} when sending a {method} to {url} with headers {kwargs.get('headers')}" + ) + raise err diff --git a/dpytools/logger/logger.py b/dpytools/logger/logger.py index ba3db7c..fcb5a1b 100644 --- a/dpytools/logger/logger.py +++ b/dpytools/logger/logger.py @@ -1,2 +1,158 @@ +import json +import traceback +from datetime import datetime, timezone +from typing import Dict, List, Optional -logger = "I will be the logger" \ No newline at end of file +import structlog + + +def level_to_severity(level: int) -> int: + """ + Helper to convert logging level to severity, please + see: https://github.com/ONSdigital/dp-standards/blob/main/LOGGING_STANDARDS.md#severity-levels + """ + if level > 40: + return 0 + elif level > 30: + return 1 + elif level > 20: + return 2 + else: + return 3 + + +def create_error_dict(error: Exception) -> List[Dict]: + """ + Take a python Exception and create a sub dict/document + matching DP logging standards. + """ + + # Note: "stack trace" guidance is very go orientated, + # this will be fine for now. + error_dict = { + "message": str(error), + "stack_trace": traceback.format_exc().split("\n"), + } + + # Listify in keeping with expected DP logging structures + return [error_dict] + + +def dp_serializer(event_log, **kw) -> Dict: + """ + Simple serialiser to align structlog defaults + with output expected by: + https://github.com/ONSdigital/dp-standards/blob/main/LOGGING_STANDARDS.md + """ + + # Note: literally just avoiding also logging the superfluous top level + # "event" key - we just want its contents + return json.dumps(event_log["event"], **kw) + + +class DpLogger: + def __init__(self, namespace: str, test_mode: bool = False): + """ + Simple python logger to create structured logs in keeping + with https://github.com/ONSdigital/dp-standards/blob/main/LOGGING_STANDARDS.md + + namespace: (required) the namespace for the app in question + test_mode: FOR USAGE DURING TESTING ONLY, makes logging statements return their structured logs. + """ + structlog.configure( + processors=[structlog.processors.JSONRenderer(dp_serializer)] + ) + self._logger = structlog.stdlib.get_logger() + self.namespace = namespace + self.test_mode = test_mode + + def _log( + self, + event, + level, + error: Optional[List] = None, + data: Optional[Dict] = None, + raw: str = None, + ): + log_entry = self._create_log_entry(event, level, data, error, raw) + self._logger.log(level, log_entry) + + if self.test_mode: + return log_entry + + def _create_log_entry(self, event, level, data, error, raw) -> Dict: + log_entry = { + "created_at": datetime.now(timezone.utc).isoformat(), + "namespace": self.namespace, + "event": event, + "trace_id": "not-implemented", + "span_id": "not-implemented", + "severity": level_to_severity(level), + "data": data if data is not None else {}, + } + + if error: + log_entry["errors"] = create_error_dict(error) + + if raw: + log_entry["raw"] = raw + + return log_entry + + def debug(self, event: str, raw: str = None, data: Dict = None): + """ + Log at the debug level. + + event: the thing that's happened, a simple short english statement + raw : a raw string of any log messages captured for a third party library + data : arbitrary key-value pairs that may be of use in providing context + """ + self._log(event, 10, raw=raw, data=data) + + def info(self, event: str, raw: str = None, data: Dict = None): + """ + Log at the info level. + + event: the thing that's happened, a simple short english statement + raw : a raw string of any log messages captured for a third party library + data : arbitrary key-value pairs that may be of use in providing context + """ + self._log(event, 20, raw=raw, data=data) + + def warning(self, event: str, raw: str = None, data: Dict = None): + """ + Log at the warning level. + + event: the thing that's happened, a simple short english statement + raw : a raw string of any log messages captured for a third party library + data : arbitrary key-value pairs that may be of use in providing context + """ + self._log(event, 30, raw=raw, data=data) + + def error(self, event: str, error: Exception, raw: str = None, data: Dict = None): + """ + Log at the error level. + + event: the thing that's happened, a simple short english statement + error: a caught python Exceotion + raw : a raw string of any log messages captured for a third party library + data : arbitrary key-value pairs that may be of use in providing context + """ + self._log(event, 40, error=error, raw=raw, data=data) + + def critical( + self, event: str, error: Exception, raw: str = None, data: Dict = None + ): + """ + IMPORTANT: You should only be logging at the critical level during + application failure, i.e if you're app is not in this process of falling + over you should not be logging a critical. + + Log at the critical level. + + event: the thing that's happened, a simple short english statement + error: a caught python Exceotion + raw : a raw string of any log messages captured for a third party library + data : arbitrary key-value pairs that may be of use in providing context + """ + self._log(event, 50, error=error, raw=raw, data=data) diff --git a/dpytools/slack/slack.py b/dpytools/slack/slack.py index dc826c5..efaaf3a 100644 --- a/dpytools/slack/slack.py +++ b/dpytools/slack/slack.py @@ -1,14 +1,32 @@ +import logging + +from dpytools.http_clients.base import BaseHttpClient + class SlackNotifier: + def __init__(self, webhook_url): + if not webhook_url: + raise ValueError("webhook_url is not set") + self.webhook_url = webhook_url + self.http_client = BaseHttpClient() + + def notify(self, msg_dict: dict): + """ + Send a message to the Slack webhook. + + The msg_dict parameter should be a dictionary that matches the + structure documented at https://api.slack.com/messaging/webhooks + """ + try: + response = self.http_client.post(self.webhook_url, json=msg_dict) + response.raise_for_status() + except Exception as e: + logging.error(f"Failed to send notification: {e}") + + def msg_str(self, msg: str): + """ + Send a string message to the Slack webhook. - def __init__(self): - # Set a webhok via an env var, ask Mike for a - #web hook url. - ... - - def notify(self, msg: str): - # Check formatting options for messages to slack. - # From memory you style it via sending a dictionary. - # It's a post request so do use the http client - # we've developing elsewhere in this library. - ... \ No newline at end of file + The msg parameter is wrapped into a dictionary before being sent. + """ + self.notify({"text": msg}) diff --git a/dpytools/sns/sns.py b/dpytools/sns/sns.py index ab20c43..0c50a2b 100644 --- a/dpytools/sns/sns.py +++ b/dpytools/sns/sns.py @@ -1,4 +1,3 @@ - # See here: https://docs.aws.amazon.com/code-library/latest/ug/python_3_sns_code_examples.html # Ideally we want to publish a message by passing in a dictionary. # If SNS doesn not support that stringify any dict that is a message @@ -12,12 +11,11 @@ from typing import Union + # Note: return True if it works and False if we hit errors # (so we can control the flow in calling programs) def publish(topic: str, msg: Union[str, dict]) -> bool: - """ - - """ + """ """ # For this you'll want boto3 again, create a subscription @@ -25,7 +23,6 @@ def publish(topic: str, msg: Union[str, dict]) -> bool: # The get_message() needs to pull from the queue that's # been subscribed to. class Subscription: - def __init__(self, topic): """ subscrube to a topic (i.e setup to read messages) diff --git a/dpytools/validation/json/validation.py b/dpytools/validation/json/validation.py new file mode 100644 index 0000000..3f63864 --- /dev/null +++ b/dpytools/validation/json/validation.py @@ -0,0 +1,91 @@ +import json +from pathlib import Path +from typing import Dict, Optional, Union +from urllib.parse import urlparse + +import jsonschema +from jsonschema import ValidationError + + +def validate_json_schema( + schema_path: Union[Path, str], + data_dict: Optional[Dict] = None, + data_path: Optional[Union[Path, str]] = None, + error_msg: Optional[str] = None, + indent: Optional[int] = None, +): + """ + Validate a JSON file against a schema. + + Either `data_dict` or `data_path` must be provided. + + `error_msg` and `indent` can be used to format the error message if validation fails. + """ + # Confirm that *either* `data_dict` *or* `data_path` has been provided, otherwise raise ValueError + if data_dict and data_path: + raise ValueError( + "Both a dictionary and file path of data have been provided - please specify either one or the other, not both." + ) + if data_dict is None and data_path is None: + raise ValueError( + "Please provide either a dictionary or a file path of the data to be validated against the schema." + ) + + # Load schema as dict + if isinstance(schema_path, str): + parsed_schema_path = urlparse(schema_path) + if parsed_schema_path.scheme == "http": + # TODO Load schema from URL + raise NotImplementedError("Validation from remote schema not yet supported") + # Convert `schema_path` to pathlib.Path + schema_path = Path(schema_path).absolute() + # Check `schema_path` exists + if not schema_path.exists(): + raise ValueError(f"Schema path '{schema_path}' does not exist") + with open(schema_path, "r") as f: + schema_from_path = json.load(f) + + # Load data to be validated + if data_dict: + if not isinstance(data_dict, Dict): + raise ValueError( + "Invalid data format, `data_dict` should be a Python dictionary" + ) + data_to_validate = data_dict + + if data_path: + # Convert `data_path` to pathlib.Path + if isinstance(data_path, str): + data_path = Path(data_path).absolute() + if not isinstance(data_path, Path): + raise ValueError( + "Invalid data format, `data_path` should be a pathlib.Path or string of file location" + ) + # Check `data_path` exists + if not data_path.exists(): + raise ValueError(f"Data path '{data_path}' does not exist") + with open(data_path, "r") as f: + data_to_validate = json.load(f) + + # Validate data against schema + try: + jsonschema.validate(data_to_validate, schema_from_path) + # TODO Handle jsonschema.SchemaError? + except jsonschema.ValidationError as err: + # If error is in a specific field, get the JSON path of the error location + if err.json_path != "$": + error_location = err.json_path + else: + error_location = "JSON data" + # Create formatted message to be output on ValidationError + if error_msg or indent: + formatted_msg = f""" +Exception: {error_msg} +Exception details: {err.message} +Exception location: {error_location} +JSON data: +{json.dumps(data_to_validate, indent=indent)} +""" + print(formatted_msg) + raise ValidationError(formatted_msg) from err + raise err diff --git a/poetry.lock b/poetry.lock index 707fccd..3e2a63a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,30 +1,64 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] [[package]] name = "black" -version = "23.11.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, - {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, - {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, - {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, - {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, - {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, - {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, - {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, - {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, - {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, - {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, - {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, - {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, - {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, - {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, - {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, - {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, - {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -38,10 +72,120 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -69,63 +213,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [package.dependencies] @@ -148,6 +292,17 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -161,20 +316,52 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" [[package]] name = "mypy-extensions" @@ -200,39 +387,39 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {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.0.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, - {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -241,13 +428,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -279,32 +466,193 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "referencing" +version = "0.33.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, + {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.32.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, + {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rpds-py" +version = "0.17.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"}, + {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"}, + {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"}, + {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"}, + {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"}, + {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"}, + {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"}, + {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"}, + {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"}, + {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"}, + {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"}, + {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"}, +] + [[package]] name = "ruff" -version = "0.1.6" +version = "0.1.15" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "structlog" +version = "23.3.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "structlog-23.3.0-py3-none-any.whl", hash = "sha256:d6922a88ceabef5b13b9eda9c4043624924f60edbb00397f4d193bd754cde60a"}, + {file = "structlog-23.3.0.tar.gz", hash = "sha256:24b42b914ac6bc4a4e6f716e82ac70d7fb1e8c3b1035a765591953bfc37101a5"}, ] +[package.extras] +dev = ["structlog[tests,typing]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + [[package]] name = "tomli" version = "2.0.1" @@ -318,16 +666,33 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "urllib3" +version = "2.2.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "3f9b13c7807ad36f5361e3460bf1ebefb3015de5d909df5cfa63e118fc445442" +python-versions = ">=3.9, <3.12" +content-hash = "3d46b73402bc0d35859f3e0e706ee178c9fcd53507b97268c128331d7e40498a" diff --git a/pyproject.toml b/pyproject.toml index 53e9b28..56d52d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "dp-python-tools" +name = "dpytools" version = "0.1.0" description = "Simple reusable python resources for digital publishing" authors = ["Your Name "] @@ -7,7 +7,12 @@ license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.9" +python = ">=3.9, <3.12" +structlog = "^23.2.0" +jsonschema = "^4.21.1" +requests = "^2.32.0" +pytest = "^7.4.4" +backoff = "^2.2.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" diff --git a/tests/coverage.rc b/tests/coverage.rc new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/log_output.py b/tests/fixtures/log_output.py new file mode 100644 index 0000000..03e2e4b --- /dev/null +++ b/tests/fixtures/log_output.py @@ -0,0 +1,13 @@ +import pytest +import structlog +from structlog.testing import LogCapture + + +@pytest.fixture(name="log_output") +def fixture_log_output(): + return LogCapture() + + +@pytest.fixture(autouse=True) +def fixture_configure_structlog(log_output): + structlog.configure(processors=[log_output]) diff --git a/tests/test_cases/logging_schema.json b/tests/test_cases/logging_schema.json new file mode 100644 index 0000000..9f2d475 --- /dev/null +++ b/tests/test_cases/logging_schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "event": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "event": { + "type": "string" + }, + "trace_id": { + "type": "string" + }, + "span_id": { + "type": "string" + }, + "severity": { + "type": "integer" + }, + "data": { + "type": "object" + }, + "raw": { + "type": "string" + } + }, + "required": [ + "created_at", + "namespace", + "event", + "trace_id", + "span_id", + "severity" + ] + }, + "log_level": { + "type": "string" + } + }, + "required": [ + "event", + "log_level" + ] +} \ No newline at end of file diff --git a/tests/test_cases/logging_schema_with_error.json b/tests/test_cases/logging_schema_with_error.json new file mode 100644 index 0000000..42bdaeb --- /dev/null +++ b/tests/test_cases/logging_schema_with_error.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "event": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "event": { + "type": "string" + }, + "trace_id": { + "type": "string" + }, + "span_id": { + "type": "string" + }, + "severity": { + "type": "integer" + }, + "data": { + "type": "object" + }, + "raw": { + "type": "string" + }, + "errors": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "stack_trace": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "message", + "stack_trace" + ] + } + ] + } + }, + "required": [ + "created_at", + "namespace", + "event", + "trace_id", + "span_id", + "severity", + "errors" + ] + }, + "log_level": { + "type": "string" + } + }, + "required": [ + "event", + "log_level" + ] +} \ No newline at end of file diff --git a/tests/test_cases/pipeline_config.json b/tests/test_cases/pipeline_config.json new file mode 100644 index 0000000..46e38c9 --- /dev/null +++ b/tests/test_cases/pipeline_config.json @@ -0,0 +1,20 @@ +{ + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [ + { + "matches": "*.sdmx", + "count": 1 + } + ], + "supplementary_distributions": [ + { + "matches": "*.sdmx", + "count": 1 + } + ], + "priority": 1, + "contact": [ + "jobloggs@ons.gov.uk" + ], + "pipeline": "default" +} \ No newline at end of file diff --git a/tests/test_cases/pipeline_config_invalid_data_type.json b/tests/test_cases/pipeline_config_invalid_data_type.json new file mode 100644 index 0000000..7c4e7b4 --- /dev/null +++ b/tests/test_cases/pipeline_config_invalid_data_type.json @@ -0,0 +1,20 @@ +{ + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [ + { + "matches": "*.sdmx", + "count": 1 + } + ], + "supplementary_distributions": [ + { + "matches": "*.sdmx", + "count": "1" + } + ], + "priority": 1, + "contact": [ + "jobloggs@ons.gov.uk" + ], + "pipeline": "default" +} \ No newline at end of file diff --git a/tests/test_cases/pipeline_config_missing_required_field.json b/tests/test_cases/pipeline_config_missing_required_field.json new file mode 100644 index 0000000..e320844 --- /dev/null +++ b/tests/test_cases/pipeline_config_missing_required_field.json @@ -0,0 +1,19 @@ +{ + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [ + { + "matches": "*.sdmx", + "count": 1 + } + ], + "supplementary_distributions": [ + { + "matches": "*.sdmx", + "count": 1 + } + ], + "contact": [ + "jobloggs@ons.gov.uk" + ], + "pipeline": "default" +} \ No newline at end of file diff --git a/tests/test_cases/pipeline_config_schema.json b/tests/test_cases/pipeline_config_schema.json new file mode 100644 index 0000000..e6cae62 --- /dev/null +++ b/tests/test_cases/pipeline_config_schema.json @@ -0,0 +1,72 @@ +{ + "$id": "airflow.schemas.ingress.sdmx.v1.schema.json", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "schema": { + "type": "string" + }, + "required_files": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "matches": { + "type": "string" + }, + "count": { + "type": "integer" + } + }, + "required": [ + "matches", + "count" + ] + } + ] + }, + "supplementary_distributions": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "matches": { + "type": "string" + }, + "count": { + "type": "integer" + } + }, + "required": [ + "matches", + "count" + ] + } + ] + }, + "priority": { + "type": "integer" + }, + "contact": { + "type": "array", + "items": [ + { + "type": "string" + } + ] + }, + "pipeline": { + "type": "string" + } + }, + "required": [ + "schema", + "required_files", + "supplementary_distributions", + "priority", + "contact", + "pipeline" + ] +} \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..3f8934c --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,116 @@ +from _pytest.monkeypatch import monkeypatch +import pytest + +from dpytools.config.config import Config +from dpytools.config.properties.string import StringProperty +from dpytools.config.properties.intproperty import IntegerProperty + +def test_config_loader(monkeypatch): + """ + Tests that a config object can be created and its attributes + dynamically generated from an input config dictionary with the + expected contents. + """ + + # Assigning environment variable values for config dictionary values + monkeypatch.setenv("SOME_STRING_ENV_VAR", "Some string value") + monkeypatch.setenv("SOME_URL_ENV_VAR", "https://test.com/some-url") + monkeypatch.setenv("SOME_INT_ENV_VAR", "6") + + config_dictionary = { + "SOME_STRING_ENV_VAR": { + "class": StringProperty, + "property": "name1", + "kwargs": { + "regex": "string value", + "min_len": 10 + }, + }, + "SOME_URL_ENV_VAR": { + "class": StringProperty, + "property": "name2", + "kwargs": { + "regex": "https://.*", + "max_len": 100 + }, + }, + "SOME_INT_ENV_VAR": { + "class": IntegerProperty, + "property": "name3", + "kwargs": { + "min_val": 5, + "max_val": 27 + } + }, +} + + config = Config.from_env(config_dictionary) + + # Assertions + + assert config.name1.name == "name1" + assert config.name1.value == "Some string value" + assert config.name1.min_len == 10 + assert config.name1.regex == "string value" + + assert config.name2.name == "name2" + assert config.name2.value == "https://test.com/some-url" + assert config.name2.regex == "https://.*" + assert config.name2.max_len == 100 + + assert config.name3.name == "name3" + assert config.name3.min_val == 5 + assert config.name3.max_val == 27 + + +def test_config_loader_no_values_error(): + """ + Tests that an exception will be raised when a config object + is created using the from_env() method but the environment + variable values have not been assigned (values are None). + """ + + # No environment variable values assigned in this test + + config_dictionary = { + "SOME_STRING_ENV_VAR": { + "class": StringProperty, + "property": "name1", + "kwargs": { + "min_len": 10 + }, + } +} + + with pytest.raises(Exception) as e: + + config = Config.from_env(config_dictionary) + + assert 'Required environment value "SOME_STRING_ENV_VAR" could not be found.' in str(e.value) + + +def test_config_loader_incorrect_type_error(monkeypatch): + """ + Tests that a TypeError will be raised when a config object + is created using the from_env() method but the type of an + attribute being created is not either a StringProperty or IntegerProperty. + """ + + monkeypatch.setenv("SOME_STRING_ENV_VAR", "Some string value") + + config_dictionary = { + "SOME_STRING_ENV_VAR": { + "class": int, + "property": "name1", + "kwargs": { + "min_val": 10, + + }, + } +} + + with pytest.raises(TypeError) as e: + + config = Config.from_env(config_dictionary) + + assert "Unsupported property type specified via 'property' field, got . Should be of type StringProperty or IntegerProperty" in str(e.value) \ No newline at end of file diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 0000000..a57532c --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,71 @@ +import pytest +from unittest.mock import patch, MagicMock +from requests import HTTPError, Response +from dpytools.http_clients.base import BaseHttpClient + + +# Mock the requests.request method +@patch("requests.request") +def test_get(mock_request): + """ + Test that the get method returns a response object + """ + + # Create a mock response object + mock_response = MagicMock(Response) + mock_response.status_code = 200 + mock_response.content = b"Test response content" + mock_request.return_value = mock_response + + # Create an instance of BaseHttpClient and make a GET request + client = BaseHttpClient() + response = client.get("http://example.com") + + # Assertions to check the response status, content and the request call + assert response.status_code == 200 + assert response.content.decode() == "Test response content" + mock_request.assert_called_once_with("GET", "http://example.com") + + +@patch("requests.request") +def test_post(mock_request): + """ + Test that the post method returns a response object + """ + + # Create a mock response object + mock_response = MagicMock(Response) + mock_response.status_code = 200 + mock_response.content = b"Test response content" + mock_request.return_value = mock_response + + # Create an instance of BaseHttpClient and make a POST request + client = BaseHttpClient() + response = client.post("http://example.com") + + # Assertions to check the response status, content and the request call + assert response.status_code == 200 + assert response.content.decode() == "Test response content" + mock_request.assert_called_once_with("POST", "http://example.com") + + +@patch("requests.request") +def test_backoff_on_exception(mock_request): + """ + Test that the get method retries on HTTPError + """ + + # Create a mock response object + mock_response = MagicMock(Response) + mock_response.status_code = 200 + + # Raise HTTPError on the first call, then return the mock_response + mock_request.side_effect = [HTTPError("HTTP Error"), mock_response] + + # Create an instance of BaseHttpClient and make a GET request + client = BaseHttpClient() + response = client.get("http://example.com") + + # Assertions to check the response status and the number of request calls + assert response.status_code == 200 + assert mock_request.call_count == 2 diff --git a/tests/test_intproperty.py b/tests/test_intproperty.py new file mode 100644 index 0000000..04efea6 --- /dev/null +++ b/tests/test_intproperty.py @@ -0,0 +1,104 @@ +import pytest +from dpytools.config.properties import IntegerProperty + +def test_int_property(): + """ + Tests if an integer property instance can be created + and validated with no errors. + """ + + test_property = IntegerProperty( + _name = "Test Integer property", + _value = 24, + min_val = 0, + max_val = 101 + ) + + test_property.type_is_valid() + test_property.secondary_validation() + + assert test_property.name == "Test Integer property" + assert test_property.value == 24 + assert test_property.min_val == 0 + assert test_property.max_val == 101 + + +def test_int_property_type_invalid(): + """ + Tests if an integer property with a type of value that + cannot be cast to string raises an exception. + """ + + test_property = IntegerProperty( + _name = "Test Integer Property", + _value = "Not an integer", + min_val = 0, + max_val = 101 + ) + + with pytest.raises(Exception) as e: + + test_property.type_is_valid() + + assert "Cannot cast Test Integer Property value Not an integer to integer." in str(e.value) + + +def test_int_property_empty_val(): + """ + Tests if an integer property with nothing as the value + raises the expected exception from the secondary validation. + """ + + test_property = IntegerProperty( + _name = "Test Integer Property", + _value = None, + min_val = 0, + max_val = 101 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert "Integer value for Test Integer property does not exist." in str(e.value) + + +def test_int_property_min_val(): + """ + Tests if an integer property with a value lower than the allowed minimum + raises the expected exception from the secondary validation. + """ + + test_property = IntegerProperty( + _name = "Test Integer Property", + _value = 9, + min_val = 10, + max_val = 101 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert "Integer value for Test Integer property is lower than allowed minimum." in str(e.value) + + + +def test_int_property_max_val(): + """ + Tests if an integer property with a value higher than the allowed maximum + raises the expected exception from the secondary validation. + """ + + test_property = IntegerProperty( + _name = "Test Integer Property", + _value = 102, + min_val = 0, + max_val = 101 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert "Integer value for Test Integer property is higher than allowed maximum." in str(e.value) \ No newline at end of file diff --git a/tests/test_json_validation.py b/tests/test_json_validation.py new file mode 100644 index 0000000..4060854 --- /dev/null +++ b/tests/test_json_validation.py @@ -0,0 +1,229 @@ +from pathlib import Path +from jsonschema import ValidationError + +import pytest +from dpytools.validation.json.validation import validate_json_schema + + +def test_validate_json_schema_data_path(): + """ + Validate data (as file path) against schema + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = "tests/test_cases/pipeline_config.json" + assert ( + validate_json_schema( + schema_path=pipeline_config_schema, + data_path=pipeline_config, + ) + is None + ) + + +def test_validate_json_schema_data_dict(): + """ + Validate data (as dictionary) against schema + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = { + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [{"matches": "*.sdmx", "count": 1}], + "supplementary_distributions": [{"matches": "*.sdmx", "count": 1}], + "priority": 1, + "contact": ["jobloggs@ons.gov.uk"], + "pipeline": "default", + } + assert ( + validate_json_schema( + schema_path=pipeline_config_schema, + data_dict=pipeline_config, + ) + is None + ) + + +def test_validate_json_schema_data_dict_and_data_path(): + """ + Raise ValueError if both `data_dict` and `data_path` are provided + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config_path = "tests/test_cases/pipeline_config.json" + pipeline_config_dict = { + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [{"matches": "*.sdmx", "count": 1}], + "supplementary_distributions": [{"matches": "*.sdmx", "count": 1}], + "priority": 1, + "contact": ["jobloggs@ons.gov.uk"], + "pipeline": "default", + } + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + data_dict=pipeline_config_dict, + data_path=pipeline_config_path, + ) + assert ( + "Both a dictionary and file path of data have been provided - please specify either one or the other, not both." + in str(err.value) + ) + + +def test_validate_json_schema_no_data_dict_or_data_path(): + """ + Raise ValueError if neither `data_dict` or `data_path` are provided + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + ) + assert ( + "Please provide either a dictionary or a file path of the data to be validated against the schema." + in str(err.value) + ) + + +def test_validate_json_schema_invalid_data_path_format(): + """ + Raise ValueError if data_path is not a string or file path + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = ["Invalid", "data", "format"] + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, data_path=pipeline_config + ) + assert ( + "Invalid data format, `data_path` should be a pathlib.Path or string of file location" + in str(err.value) + ) + + +def test_validate_json_schema_invalid_data_dict_format(): + """ + Raise ValueError if data_dict is not a dictionary + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = ["Invalid", "data", "format"] + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, data_dict=pipeline_config + ) + assert "Invalid data format, `data_dict` should be a Python dictionary" in str( + err.value + ) + + +def test_validate_json_schema_url(): + """ + Raise NotImplementedError if `schema_path` is a URL (i.e. not a local file) + """ + pipeline_config_schema = "http://example.org" + pipeline_config = "tests/test_cases/pipeline_config.json" + with pytest.raises(NotImplementedError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, data_path=pipeline_config + ) + assert "Validation from remote schema not yet supported" in str(err.value) + + +def test_validate_json_schema_invalid_schema_path(): + """ + Raise ValueError if `schema_path` does not exist + """ + pipeline_config_schema = "tests/test_cases/does_not_exist.json" + pipeline_config = "tests/test_cases/pipeline_config.json" + schema_path = Path(pipeline_config_schema).absolute() + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, data_path=pipeline_config + ) + assert f"Schema path '{schema_path}' does not exist" in str(err.value) + + +def test_validate_json_schema_invalid_data_path(): + """ + Raise ValueError if `data_path` does not exist + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = "tests/test_cases/does_not_exist.json" + data_path = Path(pipeline_config).absolute() + with pytest.raises(ValueError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, data_path=pipeline_config + ) + assert f"Data path '{data_path}' does not exist" in str(err.value) + + +def test_validate_json_schema_data_path_required_field_missing(): + """ + Raises ValidationError due to missing field in `data_path` JSON + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = "tests/test_cases/pipeline_config_missing_required_field.json" + with pytest.raises(ValidationError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + data_path=pipeline_config, + error_msg="Error validating pipeline_config_missing_required_field.json", + ) + assert "'priority' is a required property" in str(err.value) + + +def test_validate_json_schema_data_path_invalid_data_type(): + """ + Raises ValidationError due to invalid data type in `data_path` JSON + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = "tests/test_cases/pipeline_config_invalid_data_type.json" + with pytest.raises(ValidationError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + data_path=pipeline_config, + error_msg="Error validating pipeline_config_invalid_data_type.json", + ) + assert "'1' is not of type 'integer'" in str(err.value) + + +def test_validate_json_schema_data_dict_required_field_missing(): + """ + Raises ValidationError due to missing field in `data_dict` + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = { + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [{"matches": "*.sdmx", "count": 1}], + "supplementary_distributions": [{"matches": "*.sdmx", "count": 1}], + "contact": ["jobloggs@ons.gov.uk"], + "pipeline": "default", + } + with pytest.raises(ValidationError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + data_dict=pipeline_config, + error_msg="Error validating pipeline_config with required field missing", + ) + assert "'priority' is a required property" in str(err.value) + + +def test_validate_json_schema_data_dict_invalid_data_type(): + """ + Raises ValidationError due to invalid data type in `data_dict` + """ + pipeline_config_schema = "tests/test_cases/pipeline_config_schema.json" + pipeline_config = { + "schema": "airflow.schemas.ingress.sdmx.v1.schema.json", + "required_files": [{"matches": "*.sdmx", "count": 1}], + "supplementary_distributions": [{"matches": "*.sdmx", "count": "1"}], + "priority": 1, + "contact": ["jobloggs@ons.gov.uk"], + "pipeline": "default", + } + with pytest.raises(ValidationError) as err: + validate_json_schema( + schema_path=pipeline_config_schema, + data_dict=pipeline_config, + error_msg="Error validating pipeline_config dict with invalid data type", + ) + assert "'1' is not of type 'integer'" in str(err.value) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..138237c --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,127 @@ +import json +from dpytools.logger.logger import DpLogger +import pytest +import structlog +from structlog.testing import LogCapture +import jsonschema + +# TODO Add tests for http and auth fields + + +# TODO Figure out how to move fixtures to tests.fixtures.log_output.py +@pytest.fixture(name="log_output") +def fixture_log_output(): + return LogCapture() + + +@pytest.fixture(autouse=True) +def fixture_configure_structlog(log_output): + structlog.configure(processors=[log_output]) + + +# Create logger +# test_mode=True returns the dictionary being logged from the logging statement. +logger = DpLogger("test-1", test_mode=True) + + +def do_something(level: str): + if level == "debug": + logger.debug("Debug") + elif level == "arbitrary": + logger.debug("Debug", data={"arbitrary_key": "arbitrary_value"}) + elif level == "raw": + logger.debug("Debug", raw="raw") + elif level == "info": + logger.info("Info") + elif level == "warning": + logger.warning("Warning") + elif level == "error": + logger.error("Error", ValueError) + elif level == "critical": + logger.critical("Critical", ValueError) + + +# Schemas to validate log entries against +# Created with https://www.liquid-technologies.com/online-json-to-schema-converter +with open("tests/test_cases/logging_schema.json", "r") as fp: + schema = json.load(fp) + +with open("tests/test_cases/logging_schema_with_error.json", "r") as fp: + schema_with_error = json.load(fp) + + +def test_debug_no_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + a debug log. + """ + do_something("debug") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema) is None + + +def test_info_no_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + an info log. + """ + do_something("info") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema) is None + + +def test_warning_no_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + a warning log. + """ + do_something("warning") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema) is None + + +def test_error_no_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + an error log. + """ + do_something("error") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema_with_error) is None + + +def test_critical_no_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + a critical log. + """ + do_something("critical") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema_with_error) is None + + +def test_debug_with_arbitrary_data(log_output): + """ + Test that we get the expected structure when constructing + a debug log with arbitrary data. + """ + do_something("arbitrary") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema) is None + + +def test_debug_with_raw_data(log_output): + """ + Test that we get the expected structure when constructing + a debug log with raw data. + """ + do_something("raw") + output_dict = log_output.entries[0] + # Validate output_dict against schema + assert jsonschema.validate(instance=output_dict, schema=schema) is None diff --git a/tests/test_slack.py b/tests/test_slack.py new file mode 100644 index 0000000..32b85db --- /dev/null +++ b/tests/test_slack.py @@ -0,0 +1,35 @@ +import pytest +from unittest.mock import patch, MagicMock +from requests import HTTPError, Response +from dpytools.http_clients.base import BaseHttpClient +from dpytools.slack.slack import SlackNotifier + +@patch.object(BaseHttpClient, 'post') +def test_notify(mock_post): + """ + Test that the notify method sends a POST request + """ + webhook_url = 'http://example.com' + mock_response = MagicMock(Response) + mock_response.status_code = 200 + mock_post.return_value = mock_response + + notifier = SlackNotifier(webhook_url) + notifier.notify({'text': 'Test message'}) + + mock_post.assert_called_once_with(webhook_url, json={'text': 'Test message'}) + +@patch.object(BaseHttpClient, 'post') +def test_msg_str(mock_post): + """ + Test that the msg_str method sends a POST request with a string message + """ + webhook_url = 'http://example.com' + mock_response = MagicMock(Response) + mock_response.status_code = 200 + mock_post.return_value = mock_response + + notifier = SlackNotifier(webhook_url) + notifier.msg_str('Test message') + + mock_post.assert_called_once_with(webhook_url, json={'text': 'Test message'}) \ No newline at end of file diff --git a/tests/test_stringproperty.py b/tests/test_stringproperty.py new file mode 100644 index 0000000..a976980 --- /dev/null +++ b/tests/test_stringproperty.py @@ -0,0 +1,111 @@ +import pytest +from dpytools.config.properties.string import StringProperty + +def test_string_property(): + """ + Tests if a string property instance can be created + and validated with no errors. + """ + + test_property = StringProperty( + _name = "Test String Property", + _value = "Test string value", + regex = "Test", + min_len = 1, + max_len = 40 + ) + + test_property.secondary_validation() + + assert test_property.name == "Test String Property" + assert test_property.value == "Test string value" + assert test_property.regex == "Test" + assert test_property.min_len == 1 + assert test_property.max_len == 40 + + +def test_string_property_empty_val(): + """ + Tests if a string property with an empty string as the value + raises the expected exception from the secondary validation. + """ + + test_property = StringProperty( + _name = "Test String Property", + _value = "", + regex = "Test regex", + min_len = 1, + max_len = 40 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert ( + f"Str value for Test String Property is an empty string") in str(e.value) + + +def test_string_property_min_len(): + """ + Tests if a string property instance with a non-matching minimum + length string raises the expected error from secondary validation. + """ + + test_property = StringProperty( + _name = "Test String Property", + _value = "Test string value", + regex = "Test regex", + min_len = 50, + max_len = 51 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert "Str value for Test String Property is shorter than minimum length 50" in str(e.value) + + +def test_string_property_max_len(): + """ + Tests if a string property instance with a non-matching maximum + length string raises the expected error from secondary validation. + """ + + test_property = StringProperty( + _name = "Test String Property", + _value = "Test string value", + regex = "Test regex", + min_len = 1, + max_len = 2 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert ( + "Str value for Test String Property is longer than maximum length 2") in str(e.value) + + +def test_string_property_regex_no_match(): + """ + Tests if a string property instance with a non-matching regex/value + raises the expected error from secondary validation. + """ + + test_property = StringProperty( + _name = "Test String Property", + _value = "Test string value", + regex = "Test regex", + min_len = 1, + max_len = 50 + ) + + with pytest.raises(ValueError) as e: + + test_property.secondary_validation() + + assert ( + "Str value for Test String Property does not match the given regex.") in str(e.value) \ No newline at end of file