diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 68b55bd..35dc8c2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,6 +1,6 @@ [bumpversion] commit = True -current_version = 2.4.0 +current_version = 3.0.0 tag = True tag_name = {new_version} diff --git a/.editorconfig b/.editorconfig index 71ec778..4e50a0b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,16 +10,16 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[*.md] -indent_size = 2 -indent_style = space - [LICENSE.txt] insert_final_newline = false [*.{diff,patch}] trim_trailing_whitespace = false -[*.{json,yaml,yml}] +[*.{json,md,yaml,yml}] +indent_size = 2 +indent_style = space + +[.{prettierrc,yamllint}] indent_size = 2 indent_style = space diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..79a5533 --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,26 @@ +--- +changelog: + exclude: + labels: + - duplicate + - ignore-for-release + - invalid + - maintenance + - question + - wontfix + categories: + - title: Breaking Changes 🛠 + labels: + - backwards-incompatible + - breaking + - title: Fixed Bugs 🐛 + labels: + - bug + - fix + - title: Exciting New Features 🎉 + labels: + - enhancement + - feature + - title: 👒 Dependencies + labels: + - dependencies diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 7bb5391..143622b 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -22,3 +22,4 @@ jobs: uses: broadinstitute/shared-workflows/.github/workflows/python-unit-test.yaml@v6.0.0 with: python_package_name: cert_manager + python_versions: '{ "versions": [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] }' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ee4740..524de52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - --allow-missing-credentials - id: detect-private-key - id: end-of-file-fixer - exclude: '.bumpversion.cfg' + exclude: ".bumpversion.cfg" - id: mixed-line-ending - id: name-tests-test args: diff --git a/cert_manager/__init__.py b/cert_manager/__init__.py index 38d3037..d92c686 100644 --- a/cert_manager/__init__.py +++ b/cert_manager/__init__.py @@ -4,6 +4,7 @@ from .acme import ACMEAccount from .admin import Admin from .client import Client +from .dcv import DomainControlValidation from .domain import Domain from .organization import Organization from .person import Person @@ -12,5 +13,15 @@ from .ssl import SSL __all__ = [ - "ACMEAccount", "Admin", "Client", "Domain", "Organization", "PendingError", "Person", "Report", "SMIME", "SSL" + "ACMEAccount", + "Admin", + "Client", + "Domain", + "DomainControlValidation", + "Organization", + "PendingError", + "Person", + "Report", + "SMIME", + "SSL", ] diff --git a/cert_manager/__version__.py b/cert_manager/__version__.py index cf1febe..303bb19 100644 --- a/cert_manager/__version__.py +++ b/cert_manager/__version__.py @@ -3,4 +3,4 @@ __title__ = "cert_manager" __description__ = "Python interface to the Sectigo Certificate Manager REST API" __url__ = "https://github.com/broadinstitute/python-cert_manager" -__version__ = "2.4.0" +__version__ = "3.0.0" diff --git a/cert_manager/_certificates.py b/cert_manager/_certificates.py index e8b3d17..d499c4b 100644 --- a/cert_manager/_certificates.py +++ b/cert_manager/_certificates.py @@ -32,59 +32,66 @@ class Certificates(Endpoint): def __init__(self, client, endpoint, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string endpoint: The URL of the API endpoint (ex. "/ssl") - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + endpoint: The URL of the API endpoint (ex. "/ssl") + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint=endpoint, api_version=api_version) # Set to None initially. Will be filled in by methods later. - self.__cert_types = None - self.__custom_fields = None - self.__reason_maxlen = 512 + self._cert_types = None + self._custom_fields = None + self._reason_maxlen = 512 @property def types(self): """Retrieve all certificate types that are currently available. - :return list: A list of dictionaries of certificate types + Returns: + A list of dictionaries of certificate types """ # Only go to the API if we haven't done the API call yet, or if someone # specifically wants to refresh the internal cache - if not self.__cert_types: + if not self._cert_types: url = self._url("/types") result = self._client.get(url) # Build a dictionary instead of a flat list of dictionaries - self.__cert_types = {} + self._cert_types = {} for res in result.json(): name = res["name"] - self.__cert_types[name] = {} - self.__cert_types[name]["id"] = res["id"] - self.__cert_types[name]["terms"] = res["terms"] + self._cert_types[name] = {} + self._cert_types[name]["id"] = res["id"] + self._cert_types[name]["terms"] = res["terms"] - return self.__cert_types + return self._cert_types @property def custom_fields(self): """Retrieve all custom fields defined for SSL certificates. - :return list: A list of dictionaries of custom fields + Returns: + A list of dictionaries of custom fields """ # Only go to the API if we haven't done the API call yet, or if someone # specifically wants to refresh the internal cache - if not self.__custom_fields: + if not self._custom_fields: url = self._url("/customFields") result = self._client.get(url) - self.__custom_fields = result.json() + self._custom_fields = result.json() - return self.__custom_fields + return self._custom_fields def _validate_custom_fields(self, custom_fields): """Check the structure and contents of a list of custom fields dicts. Raise exceptions if validation fails. - :raises Exception: if any of the validation steps fail + Args: + custom_fields: A list of dictionaries representing custom fields + + Raises: + CustomFieldsError: if any of the validation steps fail """ # Make sure all custom fields are valid if present custom_field_names = [f['name'] for f in self.custom_fields] @@ -114,9 +121,11 @@ def collect(self, cert_id, cert_format): This method will raise a PendingError exception if the certificate is still in a pending state. - :param int cert_id: The certificate ID - :param str cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats* - :return str: the string representing the certificate in the requested format + Args: + cert_id: The certificate ID + cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats* + Returns: + The string representing the certificate in the requested format """ if cert_format not in self.valid_formats: raise ValueError(f"Invalid cert format {cert_format} provided") @@ -135,16 +144,20 @@ def collect(self, cert_id, cert_format): def enroll(self, **kwargs): """Enroll a certificate request with Sectigo to generate a certificate. - :param string cert_type_name: The full cert type name - Note: the name must match names returned from the get_types() method - :param string csr: The Certificate Signing Request (CSR) - :param int term: The length, in days, for the certificate to be issued - :param int org_id: The ID of the organization in which to enroll the certificate - :param list subject_alt_names: A list of Subject Alternative Names - :param list external_requester: One or more e-mail addresses - :param list custom_fields: zero or more objects representing custom fields and their values - Note: each object must have a 'name' key and a 'value' key - :return dict: The certificate_id and the normal status messages for errors + Args: + kwargs: A dictionary of arguments to pass to the API. + Required fields are: + cert_type_name: The full cert type name + Note: the name must match names returned from the get_types() method + csr: The Certificate Signing Request (CSR) + term: The length, in days, for the certificate to be issued + org_id: The ID of the organization in which to enroll the certificate + subject_alt_names: A list of Subject Alternative Names + external_requester: One or more e-mail addresses + custom_fields: zero or more objects representing custom fields and their values + Note: each object must have a 'name' key and a 'value' key + Returns: + The certificate_id and the normal status messages for errors """ # Retrieve all the arguments cert_type_name = kwargs.get("cert_type_name") @@ -191,13 +204,17 @@ def enroll(self, **kwargs): def replace(self, **kwargs): """Replace a pre-existing certificate. - :param int cert_id: The certificate ID - :param string csr: The Certificate Signing Request (CSR) - :param string common_name: Certificate common name. - :param str reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist. - :param list subject_alt_names: A list of Subject Alternative Names. - :return: The result of the operation, "Successful" on success - :rtype: dict + Args: + kwargs: A dictionary of arguments to pass to the API. + Required fields are: + cert_id: The certificate ID + csr: The Certificate Signing Request (CSR) + common_name: Certificate common name. + reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist. + subject_alt_names: A list of Subject Alternative Names. + + Returns: + An empty dictionary on success """ # Retrieve all the arguments cert_id = kwargs.get("cert_id") @@ -222,16 +239,19 @@ def replace(self, **kwargs): def revoke(self, cert_id, reason=""): """Revoke the certificate specified by the certificate ID. - :param int cert_id: The certificate ID - :param str reason: The Reason for revocation. - Reason can be up to 512 characters and cannot be blank (i.e. empty string) - :return dict: The revocation result. "Successful" on success + Args: + cert_id: The certificate ID + reason: The Reason for revocation. + Reason can be up to 512 characters and cannot be blank (i.e. empty string) + + Returns: + An empty dictionary on success """ url = self._url(f"/revoke/{cert_id}") # Sectigo has a 512 character limit on the "reason" message, so catch that here. - if not reason or len(reason) >= self.__reason_maxlen: - raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self.__reason_maxlen} characters") + if not reason or len(reason) >= self._reason_maxlen: + raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self._reason_maxlen} characters") data = {"reason": reason} diff --git a/cert_manager/_endpoint.py b/cert_manager/_endpoint.py index bba0224..a130511 100644 --- a/cert_manager/_endpoint.py +++ b/cert_manager/_endpoint.py @@ -11,9 +11,10 @@ class Endpoint: def __init__(self, client, endpoint, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string endpoint: The API endpoint you are accessing (for example: "/ssl") - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + endpoint: The API endpoint you are accessing (for example: "/ssl") + api_version: The API version to use; the default is "v1" """ self._client = client self._api_version = api_version @@ -35,14 +36,15 @@ def api_url(self): def create_api_url(base_url, service, version): """Build the entire Certificate Manager API URL for the service and version. - :param str base_url: The base URL you have i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL - would be https://hard.cert-manager.com/api - :param str service: The API service to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would - be /ssl - :param str version: The API version to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would - be /v1 - :return: The full URL - :rtype: str + Args: + base_url: The base URL you have + i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL would be https://hard.cert-manager.com/api + service: The API service to use + i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would be /ssl + version: The API version to use + i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would be /v1 + Returns: + The full URL """ url = base_url.rstrip("/") url += "/" + service.strip("/") @@ -54,9 +56,12 @@ def create_api_url(base_url, service, version): def _url(self, *args): """Build the endpoint URL based on the API URL inside this object. - :param str suffix: The suffix of the URL you wish to create i.e. for - https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types - :return str: The full URL + Args: + args: A list of suffixes of the URL you wish to create + i.e. for https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types + + Returns: + The full URL """ url = self._api_url.rstrip("/") for suffix in args: diff --git a/cert_manager/_helpers.py b/cert_manager/_helpers.py index cc059d0..90a5bed 100644 --- a/cert_manager/_helpers.py +++ b/cert_manager/_helpers.py @@ -16,7 +16,8 @@ def traffic_log(traffic_logger=None): Note: The "DEBUG" level should *never* be used in production. - :param obj traffic_logger: a logging.Logger to use for logging messages. + Args: + traffic_logger (logging.Logger): a logging.Logger to use for logging messages. """ def decorator(func): """Wrap the actual decorator so a reference to the function can be returned.""" @@ -81,7 +82,9 @@ def version_hack(service, version="v1"): temporarily change the version to something other than what the object was initialized with so that the internal *self.api_url* will be correct. - :param version: API version string to use. If None, 'v1' + Args: + service: The API service to use. + version: API version string to use. If None, 'v1' """ def decorator(func): """Wrap the actual decorator so a reference to the function can be returned.""" @@ -125,10 +128,12 @@ def decorator(*args, **kwargs): The `size` and `position` parameters passed through `kwargs` to this function will be used by the pagination wrapper to page through results. - :param list args: Positional parameters to pass to the wrapped function - :param dict kwargs: A dictionary with any parameters to add to the request URL + Args: + args: Positional parameters to pass to the wrapped function + kwargs: A dictionary with any parameters to add to the request URL - :return obj: Yield results from the wrapped function's response for each request + Returns: + Yield (generator) results from the wrapped function's response for each request """ size = kwargs.pop("size", 200) # max seems to be 200 by default position = kwargs.pop("position", 0) # 0-.. diff --git a/cert_manager/acme.py b/cert_manager/acme.py index bfadb8c..8e4500c 100644 --- a/cert_manager/acme.py +++ b/cert_manager/acme.py @@ -30,42 +30,45 @@ def __init__(self, client, api_version="v1"): Note: The *all* method will be run on object instantiation to fetch all acme accounts - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/acme", api_version=api_version) self._api_url = self._url("/account") - self.__acme_accounts = None + self._acme_accounts = None def all(self, org_id, force=False): """Return a list of acme accounts from Sectigo. - :param bool force: If set to True, force refreshing the data from the API - :param int org_id: The ID of the organization for which to fetch data + Args: + force: If set to True, force refreshing the data from the API + org_id: The ID of the organization for which to fetch data - :return list: A list of dictionaries representing the acme accounts + Returns: + A list of dictionaries representing the acme accounts """ - if (self.__acme_accounts) and (not force): - return self.__acme_accounts + if (self._acme_accounts) and (not force): + return self._acme_accounts - self.__acme_accounts = [] + self._acme_accounts = [] result = self.find(org_id) for acct in result: - self.__acme_accounts.append(acct) + self._acme_accounts.append(acct) - return self.__acme_accounts + return self._acme_accounts @paginate def find(self, org_id, **kwargs): """Return a list of acme accounts matching the parameters. - :param int org_id: The ID of the organization for which to search - :param dict kwargs: A dictionary of additional arguments to pass to the API + Args: + org_id: The ID of the organization for which to search + kwargs: A dictionary of additional arguments to pass to the API + Any other List ACME accounts request parameters can be provided as keyword arguments. - Any other List ACME accounts request parameters can be provided as - keyword arguments. - - :return list: A list of dictionaries representing the matched acme accounts + Returns: + A list of dictionaries representing the matched acme accounts """ kwargs["org_id"] = org_id params = { @@ -80,9 +83,11 @@ def find(self, org_id, **kwargs): def get(self, acme_id): """Return a dictionary of acme account information. - :param int acme_id: The ID of the acme account to query + Args: + acme_id: The ID of the acme account to query - return dict: The account information + Returns: + A dictionary representing the acme account """ url = self._url(str(acme_id)) result = self._client.get(url) @@ -92,12 +97,14 @@ def get(self, acme_id): def create(self, name, acme_server, org_id, ev_details=None): """Create an acme account. - :param str name: The account name - :param str acme_server: The acme account server name (URL) - :param int org_id: The ID of the organization to associate the acme account with - :param dict ev_details: The EV details for the acme account + Args: + name: The account name + acme_server: The acme account server name (URL) + org_id: The ID of the organization to associate the acme account with + ev_details: The EV details for the acme account - :return dict: The creation result + Returns: + A dictionary representing the creation result """ data = { "name": name, @@ -130,10 +137,12 @@ def create(self, name, acme_server, org_id, ev_details=None): def update(self, acme_id, name): """Update an acme account. - :param int acme_id: The ID of the acme account to update - :param str name: The account name + Args: + acme_id: The ID of the acme account to update + name: The account name - :return bool: Update success or failure + Returns: + Boolean indicating update success or failure """ data = {"name": name} url = self._url(str(acme_id)) @@ -144,9 +153,11 @@ def update(self, acme_id, name): def delete(self, acme_id): """Delete an acme account. - :param int acme_id: The ID of the acme account to delete + Args: + acme_id: The ID of the acme account to delete - :return bool: Deletion success or failure + Returns: + Boolean indicating deletion success or failure """ url = self._url(str(acme_id)) result = self._client.delete(url) @@ -156,10 +167,12 @@ def delete(self, acme_id): def add_domains(self, acme_id, domains): """Add domains to an acme account. - :param int acme_id: The ID of the acme account to add domains to - :param list domains: The domains to add + Args: + acme_id: The ID of the acme account to add domains to + domains: The domains to add - :return dict: A dictionary containing a list of domains not added + Returns: + A dictionary containing a list of domains not added """ data = { "domains": [ @@ -178,10 +191,12 @@ def add_domains(self, acme_id, domains): def remove_domains(self, acme_id, domains): """Remove domains from an acme account. - :param int acme_id: The ID of the acme account to remove domains from - :param list domains: The domains to remove + Args: + acme_id: The ID of the acme account to remove domains from + domains: The domains to remove - :return dict: A dictionary containing a list of domains not removed + Returns: + A dictionary containing a list of domains not removed """ data = { "domains": [ diff --git a/cert_manager/admin.py b/cert_manager/admin.py index 4ef2a24..fa02564 100644 --- a/cert_manager/admin.py +++ b/cert_manager/admin.py @@ -20,47 +20,53 @@ class Admin(Endpoint): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/admin", api_version=api_version) - self.__admins = None + self._admins = None self.all() def all(self, force=False): """Return a list of admins from Sectigo. - :param bool force: If set to True, force refreshing the data from the API + Args: + force: If set to True, force refreshing the data from the API - :return list: A list of dictionaries representing the admins + Returns: + A list of dictionaries representing the admins """ - if (self.__admins) and (not force): - return self.__admins + if (self._admins) and (not force): + return self._admins result = self._client.get(self._api_url) - self.__admins = result.json() + self._admins = result.json() - return self.__admins + return self._admins def create(self, login, email, forename, surname, password, credentials, **kwargs): # noqa: PLR0913 """Create a new administrator. - :param str login: Login name of admin to create - :param str email: Email of admin to create - :param str forename: Fore/First name of admin - :param str surname: Sur/Last name of admin - :param list credentials: List of Credentials to apply to admin - :param dict kwargs: Additional fields that will be passed to the API - Formating for "Credentials" can be found in the Sectigo API Documentation. Additional request fields are documented in Sectigo API Documentation https://sectigo.com/faqs/detail/Sectigo-Certificate-Manager-SCM-REST-API/kA01N000000XDkE Other parameters that may be useful are privileges, identityProviderId, and idpPersonId - :return dict: The id of the created admin + Args: + login: Login name of admin to create + email: Email of admin to create + forename: Fore/First name of admin + surname: Sur/Last name of admin + password: Password for the admin + credentials: List of Credentials to apply to admin + kwargs: Additional fields that will be passed to the API + + Returns: + A dictionary containing the id of the created admin """ data = { "login": login, @@ -105,9 +111,11 @@ def create(self, login, email, forename, surname, password, credentials, **kwarg def get(self, admin_id): """Return a dictionary of admin information. - :param int admin_id: The ID of the admin to query + Args: + admin_id: The ID of the admin to query - return dict: The admin information + Returns: + A dictionary representing the admin information """ url = self._url(str(admin_id)) result = self._client.get(url) @@ -117,7 +125,8 @@ def get(self, admin_id): def get_idps(self): """Return a list of IDPs. - :return list: A list of dictionaries representing the IDPs + Returns: + A list of dictionaries representing the IDPs """ url = self._url("idp") result = self._client.get(url) @@ -127,9 +136,11 @@ def get_idps(self): def delete(self, admin_id): """Delete an admin. - :param int admin_id: The ID of the admin to delete + Args: + admin_id: The ID of the admin to delete - :return bool: Deletion success or failure + Returns: + Boolean indicating deletion success or failure """ url = self._url(str(admin_id)) result = self._client.delete(url) @@ -139,10 +150,12 @@ def delete(self, admin_id): def update(self, admin_id, **kwargs): """Update an admin. - :param int admin_id: The ID of the admin to update - :param dict kwargs: A dictionary of properties to update + Args: + admin_id: The ID of the admin to update + kwargs: A dictionary of properties to update - :return bool: Update success or failure + Returns: + Boolean indicating update success or failure """ data = {} for key, value in kwargs.items(): diff --git a/cert_manager/client.py b/cert_manager/client.py index bf4634d..b5b0785 100644 --- a/cert_manager/client.py +++ b/cert_manager/client.py @@ -16,65 +16,149 @@ class Client: # pylint: disable=too-many-instance-attributes """Serve as a Base class for calls to the Sectigo Cert Manager APIs.""" DOWNLOAD_TYPES = [ - "base64", # PKCS#7 Base64 encoded - "bin", # PKCS#7 Bin encoded - "x509", # X509, Base64 encoded - "x509CO", # X509 Certificate only, Base64 encoded - "x509IO", # X509 Intermediates/root only, Base64 encoded + "base64", # PKCS#7 Base64 encoded + "bin", # PKCS#7 Bin encoded + "x509", # X509, Base64 encoded + "x509CO", # X509 Certificate only, Base64 encoded + "x509IO", # X509 Intermediates/root only, Base64 encoded "x509IOR", # X509 Intermediates/root only Reverse, Base64 encoded ] def __init__(self, **kwargs): """Initialize the class. - :param string base_url: The full URL to the Sectigo API server; the default is "https://cert-manager.com/api" - :param string login_uri: The URI for the customer login - If your login to the Sectigo GUI is https://cert-manager.com/customer/foo/, your login URI is "foo". - :param string username: The username with which to login - :param string password: The password with which to login - :param bool cert_auth: Use client certificate authentication if True; the default is False - :param string user_crt_file: The path to the certificate file if using client cert auth - :param string user_key_file: The path to the key file if using client cert auth + Args: + kwargs: The keyword arguments to use for initialization. The following keys are accepted: + base_url: The full URL to the Sectigo API server; the default is "https://cert-manager.com/api" + login_uri: The URI for the customer login. + If your login to the Sectigo GUI is https://cert-manager.com/customer/foo/, your login URI is "foo". + username: The username with which to login + password: The password with which to login + cert_auth: Boolean on whether to use client certificate authentication if True; the default is False + user_crt_file: The path to the certificate file if using client cert auth + user_key_file: The path to the key file if using client cert auth + auth_url: The full URL to the Sectigo OAuth2 token endpoint; the default is "https://auth.sso.sectigo.com/auth/realms/apiclients/protocol/openid-connect/token" + client_id: The Client ID to use for OAuth2 authentication + client_secret: The Client Secret to use for OAuth2 authentication """ - # These options are required, so raise a KeyError if they are not provided. - self.__login_uri = kwargs["login_uri"] - self.__username = kwargs["username"] - - # Using get for consistency and to allow defaults to be easily set - self.__base_url = kwargs.get("base_url", "https://cert-manager.com/api") - self.__cert_auth = kwargs.get("cert_auth", False) - self.__session = requests.Session() - - self.__user_crt_file = kwargs.get("user_crt_file") - self.__user_key_file = kwargs.get("user_key_file") - + # Initialize class variables + self._base_url = None + self._cert_auth = None + self._login_uri = None + self._oauth2_token = None + self._password = None + self._user_crt_file = None + self._user_key_file = None + self._username = None + + self._session = requests.Session() # Set the default HTTP headers - self.__headers = { - "login": self.__username, - "customerUri": self.__login_uri, + self._headers = { "Accept": "application/json", "User-Agent": self.user_agent, } - # Setup the Session for certificate auth - if self.__cert_auth: - # Require keys if cert_auth is True or raise a KeyError - self.__user_crt_file = kwargs["user_crt_file"] - self.__user_key_file = kwargs["user_key_file"] - self.__session.cert = (self.__user_crt_file, self.__user_key_file) + if "client_id" in kwargs or "client_secret" in kwargs: + self._oauth2_login( + client_id=kwargs["client_id"], + client_secret=kwargs["client_secret"], + auth_url=kwargs.get( + "auth_url", "https://auth.sso.sectigo.com/auth/realms/apiclients/protocol/openid-connect/token" + ), + base_url=kwargs.get("base_url", "https://admin.enterprise.sectigo.com/api"), + ) + else: + # These options are required, so raise a KeyError if they are not provided. + self._login_uri = kwargs["login_uri"] + self._username = kwargs["username"] + + # Using get for consistency and to allow defaults to be easily set + self._base_url = kwargs.get("base_url", "https://cert-manager.com/api") + self._cert_auth = kwargs.get("cert_auth", False) + + # Set the default HTTP headers + self._headers.update({ + "login": self._username, + "customerUri": self._login_uri, + }) + + if self._cert_auth: + self._cert_login( + base_url=self._base_url, + login_uri=self._login_uri, + username=self._username, + user_crt_file=kwargs["user_crt_file"], + user_key_file=kwargs["user_key_file"], + ) + else: + self._password_login(username=self._username, password=kwargs["password"]) + + self._session.headers.update(self._headers) + + def _cert_login(self, base_url, login_uri, username, user_crt_file, user_key_file): + """Authenticate to the Sectigo API using client certificate authentication. + + Args: + base_url: The full URL to the Sectigo API server + login_uri: The URI for the customer login + username: The username with which to login + user_crt_file: The path to the client certificate file + user_key_file: The path to the client key file + """ + self._user_crt_file = user_crt_file + self._user_key_file = user_key_file - # Warn about using /api instead of /private/api if doing certificate auth - if not re.search("/private", self.__base_url): - cert_uri = re.sub("/api", "/private/api", self.__base_url) - LOGGER.warning("base URI should probably be %s due to certificate auth", cert_uri) + # Require keys if cert_auth is True or raise a KeyError + self._session.cert = (self._user_crt_file, self._user_key_file) - else: - # If we're not doing certificate auth, we need a password, so make sure an exception is raised if - # a password was not passed as an argument - self.__password = kwargs["password"] - self.__headers["password"] = self.__password + # Warn about using /api instead of /private/api if doing certificate auth + if not re.search("/private", self._base_url): + cert_uri = re.sub("/api", "/private/api", self._base_url) + LOGGER.warning("base URI should probably be %s due to certificate auth", cert_uri) + + def _password_login(self, username,password): + """Authenticate to the Sectigo API using password authentication. - self.__session.headers.update(self.__headers) + Args: + username: The username with which to login + password: The password with which to login + """ + # If we're not doing certificate auth, we need a password, so make sure an exception is raised if + # a password was not passed as an argument + self._password = password + self._headers.update({ + "password": self._password + }) + + def _oauth2_login( + self, + client_id, + client_secret, + auth_url="https://auth.sso.sectigo.com/auth/realms/apiclients/protocol/openid-connect/token", + base_url="https://admin.enterprise.sectigo.com/api", + ): + """Initialize the class using OAuth2. + + Args: + client_id: The Client ID to use for OAuth2 authentication + client_secret: The Client Secret to use for OAuth2 authentication + auth_url: The full URL to the Sectigo OAuth2 token endpoint; the default is "https://auth.sso.sectigo.com/auth/realms/apiclients/protocol/openid-connect/token" + base_url: The base URL for the Sectigo API; the default is "https://admin.enterprise.sectigo.com/api" + """ + self._base_url = base_url + # Using get for consistency and to allow defaults to be easily set + self._session = requests.Session() + + payload = {"client_id": client_id, "client_secret": client_secret, "grant_type": "client_credentials"} + headers = {"accept": "application/json", "content-type": "application/x-www-form-urlencoded"} + + response = requests.post(auth_url, data=payload, headers=headers) + self._oauth2_token = response.json()['access_token'] + + # Set the default HTTP headers + self._headers = { + "Authorization": f"Bearer {self._oauth2_token}", + } @property def user_agent(self): @@ -87,18 +171,18 @@ def user_agent(self): @property def base_url(self): - """Return the internal __base_url value.""" - return self.__base_url + """Return the internal _base_url value.""" + return self._base_url @property def headers(self): - """Return the internal __headers value.""" - return self.__headers + """Return the internal _headers value.""" + return self._headers @property def session(self): - """Return the setup internal __session requests.Session object.""" - return self.__session + """Return the setup internal _session requests.Session object.""" + return self._session def add_headers(self, headers=None): """Add the provided headers to the internally stored headers. @@ -106,13 +190,14 @@ def add_headers(self, headers=None): Note: This function will overwrite an existing header if the key in the headers parameter matches one of the keys in the internal dictionary of headers. - :param dict headers: A dictionary where key is the header with its value being the setting for that header. + Args: + headers: A dictionary where key is the header with its value being the setting for that header. """ if headers: - head = self.__headers.copy() + head = self._headers.copy() head.update(headers) - self.__headers = head - self.__session.headers.update(self.__headers) + self._headers = head + self._session.headers.update(self._headers) def remove_headers(self, headers=None): """Remove the requested header keys from the internally stored headers. @@ -120,24 +205,27 @@ def remove_headers(self, headers=None): Note: If any of the headers in provided the list do not exist, the header will be ignored and will not raise an exception. - :param list headers: A list of header keys to delete + Args: + headers: A list of header keys to delete """ if headers: for head in headers: - if head in self.__headers: - del self.__headers[head] - del self.__session.headers[head] + if head in self._headers: + del self._headers[head] + del self._session.headers[head] @traffic_log(traffic_logger=LOGGER) def head(self, url, headers=None, params=None): """Submit a HEAD request to the provided URL. - :param str url: A URL to query - :param dict headers: A dictionary with any extra headers to add to the request - :param dict params: A dictionary with any parameters to add to the request URL - :return obj: A requests.Response object received as a response + Args: + url: A URL to query + headers: A dictionary with any extra headers to add to the request + params: A dictionary with any parameters to add to the request URL + Returns: + A requests.Response object received as a response """ - result = self.__session.head(url, headers=headers, params=params) + result = self._session.head(url, headers=headers, params=params) # Raise an exception if the return code is in an error range result.raise_for_status() @@ -147,12 +235,14 @@ def head(self, url, headers=None, params=None): def get(self, url, headers=None, params=None): """Submit a GET request to the provided URL. - :param str url: A URL to query - :param dict headers: A dictionary with any extra headers to add to the request - :param dict params: A dictionary with any parameters to add to the request URL - :return obj: A requests.Response object received as a response + Args: + url: A URL to query + headers: A dictionary with any extra headers to add to the request + params: A dictionary with any parameters to add to the request URL + Returns: + A requests.Response object received as a response """ - result = self.__session.get(url, headers=headers, params=params) + result = self._session.get(url, headers=headers, params=params) # Raise an exception if the return code is in an error range result.raise_for_status() @@ -162,12 +252,14 @@ def get(self, url, headers=None, params=None): def post(self, url, headers=None, data=None): """Submit a POST request to the provided URL and data. - :param str url: A URL to query - :param dict headers: A dictionary with any extra headers to add to the request - :param dict data: A dictionary with the data to use for the body of the POST - :return obj: A requests.Response object received as a response + Args: + url: A URL to query + headers: A dictionary with any extra headers to add to the request + data: A dictionary with the data to use for the body of the POST + Returns: + A requests.Response object received as a response """ - result = self.__session.post(url, json=data, headers=headers) + result = self._session.post(url, json=data, headers=headers) # Raise an exception if the return code is in an error range result.raise_for_status() @@ -177,12 +269,14 @@ def post(self, url, headers=None, data=None): def put(self, url, headers=None, data=None): """Submit a PUT request to the provided URL and data. - :param str url: A URL to query - :param dict headers: A dictionary with any extra headers to add to the request - :param dict data: A dictionary with the data to use for the body of the PUT - :return obj: A requests.Response object received as a response + Args: + url: A URL to query + headers: A dictionary with any extra headers to add to the request + data: A dictionary with the data to use for the body of the PUT + Returns: + A requests.Response object received as a response """ - result = self.__session.put(url, json=data, headers=headers) + result = self._session.put(url, json=data, headers=headers) # Raise an exception if the return code is in an error range result.raise_for_status() @@ -192,12 +286,14 @@ def put(self, url, headers=None, data=None): def delete(self, url, headers=None, data=None): """Submit a DELETE request to the provided URL. - :param str url: A URL to query - :param dict headers: A dictionary with any extra headers to add to the request - :param dict data: A dictionary with the data to use for the body of the DELETE - :return obj: A requests.Response object received as a response + Args: + url: A URL to query + headers: A dictionary with any extra headers to add to the request + data: A dictionary with the data to use for the body of the DELETE + Returns: + A requests.Response object received as a response """ - result = self.__session.delete(url, json=data, headers=headers) + result = self._session.delete(url, json=data, headers=headers) # Raise an exception if the return code is in an error range result.raise_for_status() diff --git a/cert_manager/dcv.py b/cert_manager/dcv.py index a7953e5..6e9cece 100644 --- a/cert_manager/dcv.py +++ b/cert_manager/dcv.py @@ -13,21 +13,23 @@ class DomainControlValidation(Endpoint): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/dcv", api_version=api_version) def search(self, **kwargs): """Search the DCV statuses of domains. - :param dict kwargs the following search keys are supported: - position, size, domain, org, department, dcvStatus, orderStatus, expiresIn + See https://www.sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resources-dcv-statuses - See - https://www.sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resources-dcv-statuses + Args: + kwargs: The following search keys are supported: + position, size, domain, org, department, dcvStatus, orderStatus, expiresIn - :return list: a list of DCV statuses + Returns: + A list of DCV statuses """ url = self._url("validation") result = self._client.get(url, params=kwargs) @@ -37,13 +39,11 @@ def search(self, **kwargs): def get_validation_status(self, domain: str): """Get the DCV statuses of a domain. - :param dict kwargs the following search keys are supported: - position, size, domain, org, department, dcvStatus, orderStatus, expiresIn - - See - https://www.sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resources-dcv-status + Args: + domain: The domain to query - :return list: the DCV status for the domain + Returns: + A list of DCV statuses for the domain """ url = self._url("validation", "status") data = {"domain": domain} @@ -65,11 +65,13 @@ def start_validation_cname(self, domain: str): See https://www.sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resources-dcv-start-http - :param string domain: The domain to validate + Args: + domain: The domain to validate - :return response: a dictionary containing - host: Where the validation will expect the CNAME to live on the server - point: Where the CNAME should point to + Returns: + A dictionary containing: + host: Where the validation will expect the CNAME to live on the server + point: Where the CNAME should point to """ url = self._url("validation", "start", "domain", "cname") data = {"domain": domain} @@ -91,12 +93,14 @@ def submit_validation_cname(self, domain: str): See https://www.sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resources-dcv-submit-cname - :param string domain: The domain to validate + Args: + domain: The domain to validate - :return response: a dictionary containing - status: The status of the validation - orderStatus: The status of the validation request - message: An optional message to help with debugging + Returns: + A dictionary containing: + status: The status of the validation + orderStatus: The status of the validation request + message: An optional message to help with debugging """ url = self._url("validation", "submit", "domain", "cname") data = {"domain": domain} diff --git a/cert_manager/domain.py b/cert_manager/domain.py index 512d3fa..0348407 100644 --- a/cert_manager/domain.py +++ b/cert_manager/domain.py @@ -20,35 +20,40 @@ class Domain(Endpoint): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/domain", api_version=api_version) - self.__domains = None + self._domains = None def all(self, force=False): """Return a list of domains from Sectigo. - :param bool force: If set to True, force refreshing the data from the API + Args: + force: If set to True, force refreshing the data from the API - :return list: A list of dictionaries representing the domains + Returns: + A list of dictionaries representing the domains """ - if (self.__domains) and (not force): - return self.__domains + if (self._domains) and (not force): + return self._domains result = self._client.get(self._api_url) - self.__domains = result.json() + self._domains = result.json() - return self.__domains + return self._domains def find(self, **kwargs): """Return a list of domains matching the given parameters from Sectigo. - :param dict kwargs: A dictonary of parameters that will be passed to the API to execute teh search + Args: + kwargs: A dictionary of parameters that will be passed to the API to execute the search - :return list: A list of dictionaries representing the domains that match the given parameters + Returns: + A list of dictionaries representing the domains that match the given parameters """ result = self._client.get(self._api_url, params=kwargs) @@ -59,7 +64,11 @@ def count(self, **kwargs): If no parameters are given, the count will be of all domains. - :return dict: Count of domains matching the given parameters + Args: + kwargs: A dictionary of parameters that will be passed to the API to execute the search + + Returns: + A dictionary containing the count of domains matching the given parameters """ url = self._url("/count") result = self._client.get(url, params=kwargs) @@ -69,12 +78,14 @@ def count(self, **kwargs): def create(self, name, org_id, cert_types, **kwargs): """Create a domain. - :param str name: Name of domain to create - :param int org_id: Organization Id to delegate the newly created domain to - :param list cert_types: Certificate types to delegate, allowed values are "SSL", "SMIME", and "CodeSign" - :param dict kwargs: A dictionary of additional fields to pass to the API + Args: + name: Name of domain to create + org_id: Organization Id to delegate the newly created domain to + cert_types: Certificate types to delegate, allowed values are "SSL", "SMIME", and "CodeSign" + kwargs: A dictionary of additional fields to pass to the API - :return _type_: _description_ + Returns: + A dictionary containing the ID of the newly created domain """ data = { "name": name, @@ -119,9 +130,11 @@ def create(self, name, org_id, cert_types, **kwargs): def get(self, domain_id): """Return a dictionary of domain information. - :param int domain_id: The ID of the domain to query + Args: + domain_id: The ID of the domain to query - return dict: The domain information + Returns: + A dictionary containing the domain information """ url = self._url(str(domain_id)) result = self._client.get(url) @@ -131,9 +144,11 @@ def get(self, domain_id): def delete(self, domain_id): """Delete a domain. - :param int domain_id: The ID of the domain to delete + Args: + domain_id: The ID of the domain to delete - :return bool: Deletion success or failure + Returns: + bool: Deletion success or failure """ url = self._url(str(domain_id)) result = self._client.delete(url) @@ -143,9 +158,11 @@ def delete(self, domain_id): def activate(self, domain_id): """Activate a domain. - :param int domain_id: The ID of the domain to activate + Args: + domain_id: The ID of the domain to activate - :return bool: activation success or failure + Returns: + bool: Activation success or failure """ url = self._url(str(domain_id), "activate") result = self._client.put(url) @@ -155,9 +172,11 @@ def activate(self, domain_id): def suspend(self, domain_id): """Suspend a domain. - :param int domain_id: The ID of the domain to suspend + Args: + domain_id: The ID of the domain to suspend - :return bool: suspension success or failure + Returns: + bool: Suspension success or failure """ url = self._url(str(domain_id), "suspend") result = self._client.put(url) @@ -167,11 +186,13 @@ def suspend(self, domain_id): def delegate(self, domain_id, org_id, cert_types): """Delegate a domain. - :param int domain_id: The ID of the domain to delegate - :param int org_id: The ID of the organization to delegate the domain to - :param list cert_types: List of certificate types to delegate, allowed values are "SSL", "SMIME", and "CodeSign" + Args: + domain_id: The ID of the domain to delegate + org_id: The ID of the organization to delegate the domain to + cert_types: List of certificate types to delegate, allowed values are "SSL", "SMIME", and "CodeSign" - :return bool: delegation success or failure + Returns: + bool: Delegation success or failure """ url = self._url(str(domain_id), "delegation") data = { @@ -185,12 +206,14 @@ def delegate(self, domain_id, org_id, cert_types): def remove_delegation(self, domain_id, org_id, cert_types): """Remove a delegation for a domain. - :param int domain_id: The ID of the domain to remove delegation for - :param int org_id: The ID of the organization to remove delegation from - :param list cert_types: List of certificate types to remove delegation for, - allowed values are "SSL", "SMIME", and "CodeSign" + Args: + domain_id: The ID of the domain to remove delegation for + org_id: The ID of the organization to remove delegation from + cert_types: List of certificate types to remove delegation for, + allowed values are "SSL", "SMIME", and "CodeSign" - :return bool: delegation removal success or failure + Returns: + bool: Delegation removal success or failure """ url = self._url(str(domain_id), "delegation") data = { @@ -204,10 +227,12 @@ def remove_delegation(self, domain_id, org_id, cert_types): def approve_delegation(self, domain_id, org_id): """Approve a requested delegation. - :param int domain_id: The ID of the domain to approve - :param int org_id: The ID of the organization requesting the delegation + Args: + domain_id: The ID of the domain to approve + org_id: The ID of the organization requesting the delegation - :return bool: approval success or failure + Returns: + bool: approval success or failure """ url = self._url(str(domain_id), "delegation", "approve") data = { @@ -220,10 +245,12 @@ def approve_delegation(self, domain_id, org_id): def reject_delegation(self, domain_id, org_id): """Reject a requested delegation. - :param int domain_id: The ID of the domain to approve - :param int org_id: The ID of the organization requesting the delegation + Args: + domain_id: The ID of the domain to reject + org_id: The ID of the organization requesting the delegation - :return bool: True if request was rejected, False otherwise + Returns: + bool: True if request was rejected, False otherwise """ url = self._url(str(domain_id), "delegation", "reject") data = { diff --git a/cert_manager/organization.py b/cert_manager/organization.py index 5ccc7b0..25f85b2 100644 --- a/cert_manager/organization.py +++ b/cert_manager/organization.py @@ -15,29 +15,32 @@ def __init__(self, client, api_version="v1"): Note: The *all* method will be run on object instantiation to fetch all organizations - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/organization", api_version=api_version) - self.__orgs = None + self._orgs = None self.all() def all(self, force=False): """Return a list of organizations from Sectigo. - :param bool force: If set to True, force refreshing the data from the API + Args: + force: If set to True, force refreshing the data from the API - :return list: A list of dictionaries representing the organizations + Returns: + list: A list of dictionaries representing the organizations """ - if (self.__orgs) and (not force): - return self.__orgs + if (self._orgs) and (not force): + return self._orgs result = self._client.get(self._api_url) - self.__orgs = result.json() + self._orgs = result.json() - return self.__orgs + return self._orgs def find(self, org_name=None, dept_name=None): """Return a dictionary of organization information. @@ -52,10 +55,12 @@ def find(self, org_name=None, dept_name=None): If *neither* parameter is provided, all organizations will be returned (i.e. an alias for *all*) - :param str org_name: The name of the organization for which to search - :param str org_name: The name of the department for which to search + Args: + org_name: The name of the organization for which to search + dept_name: The name of the department for which to search - :return list: A list of dictionaries representing the organization or department + Returns: + list: A list of dictionaries representing the organization or department """ ret = {} diff --git a/cert_manager/person.py b/cert_manager/person.py index 5fb06d0..261a9f2 100644 --- a/cert_manager/person.py +++ b/cert_manager/person.py @@ -15,8 +15,9 @@ class Person(Endpoint): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/person", api_version=api_version) @@ -24,13 +25,16 @@ def __init__(self, client, api_version="v1"): def list(self, **kwargs): """Return a list of people in sectigo. - The 'size' and 'position' parameters passed as arguments to this function will be used + The 'size' and 'position' parameters passed as arguments to this function will be used by the pagination wrapper to page through results. All other filtering parameters can be referenced at: https://sectigo.com/knowledge-base/detail/SCM-Sectigo-Certificate-Manager-REST-API/kA01N000000XDkE - :param dict kwargs: A dictionary of arguments to pass to the API - :return iter: An iterator object is returned to cycle through the certificates + Args: + kwargs: A dictionary of arguments to pass to the API + + Returns: + iter: An iterator object is returned to cycle through the certificates """ result = self._client.get(self._api_url, params=kwargs) return result.json() @@ -38,8 +42,11 @@ def list(self, **kwargs): def find(self, email): """Return a list of people with the given email from the Sectigo API. - :param str email: The email address for which we are searching - :return list: A list of dictionaries representing the people found + Args: + email: The email address for which we are searching + + Returns: + list: A list of dictionaries representing the people found """ quoted_email = quote(email) @@ -52,8 +59,11 @@ def find(self, email): def get(self, person_id): """Returns the details of a person. - :param int person_id: The person's ID - :return dict: A dictionary of the person's details + Args: + person_id: The person's ID + + Returns: + dict: A dictionary of the person's details """ url = self._url(str(person_id)) result = self._client.get(url) @@ -62,18 +72,23 @@ def get(self, person_id): def create(self, **kwargs) -> dict: """Create a person. - :param string first_name: The person's first name - :param string middleName: The person's middle name - :param string last_name: The person's last name - :param string email: The person's e-mail - :param string validation_type: Person's validation type. Values: [STANDARD, HIGH] - :param int org_id: The ID of the organization in which to enroll the certificate - :param string phone: The person's phone number - :param string common_name: The person's common name. If ommited, constructed from person's full name - :param list secondary_emails: The person's secondary e-mail(s) - :param string eppn: The person's EPPN - :param string upn: The person's UPN (User Principal Name) - :return dict: A dict containing the 'personId' of the created person + Args: + kwargs: A dictionary of arguments to pass to the API. + Allowed fields are: + first_name: The person's first name + middleName: The person's middle name + last_name: The person's last name + email: The person's e-mail + validation_type: Person's validation type. Values: [STANDARD, HIGH] + org_id: The ID of the organization in which to enroll the certificate + phone: The person's phone number + common_name: The person's common name. If ommited, constructed from person's full name + secondary_emails: The person's secondary e-mail(s) + eppn: The person's EPPN + upn: The person's UPN (User Principal Name) + + Returns: + dict: A dict containing the 'personId' of the created person """ # Retrieve all the arguments email = kwargs.get("email") @@ -103,18 +118,21 @@ def create(self, **kwargs) -> dict: def update(self, **kwargs) -> None: """Update a person. - :param string person_id: The person's id - :param string first_name: The person's first name - :param string middleName: The person's middle name - :param string last_name: The person's last name - :param string email: The person's e-mail - :param string validation_type: Person's validation type. Values: [STANDARD, HIGH] - :param int org_id: The ID of the organization in which to enroll the certificate - :param string phone: The person's phone number - :param string common_name: The person's common name. If ommited, constructed from person's full name - :param list secondary_emails: The person's secondary e-mail(s) - :param string eppn: The person's EPPN - :param string upn: The person's UPN (User Principal Name) + Args: + kwargs: A dictionary of arguments to pass to the API. + Allowed fields are: + person_id: The person's ID + first_name: The person's first name + middleName: The person's middle name + last_name: The person's last name + email: The person's e-mail + validation_type: Person's validation type. Values: [STANDARD, HIGH] + org_id: The ID of the organization in which to enroll the certificate + phone: The person's phone number + common_name: The person's common name. If ommited, constructed from person's full name + secondary_emails: The person's secondary e-mail(s) + eppn: The person's EPPN + upn: The person's UPN (User Principal Name) """ # Retrieve all the arguments person_id = kwargs.get("person_id") @@ -140,7 +158,10 @@ def update(self, **kwargs) -> None: def delete(self, **kwargs): """Delete a person. - :param string person_id: The person's id + Args: + kwargs: A dictionary of arguments to pass to the API. + Allowed fields are: + person_id: The person's ID """ person_id = kwargs.get("person_id") self._client.delete(self._url(str(person_id))) diff --git a/cert_manager/report.py b/cert_manager/report.py index 8253442..3728f99 100644 --- a/cert_manager/report.py +++ b/cert_manager/report.py @@ -15,22 +15,24 @@ class Report(Endpoint): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/report", api_version=api_version) def get(self, report_name, **kwargs): """Get any available reports provided in the REST Sctigo API. - :param str report_name: Name of report based on the api url suffix - :param dict kwargs: Additional fields that will be passed to the API + Args: + report_name: Name of report based on the api url suffix + kwargs: Additional fields that will be passed to the API + Search fields for reports can be found in the Sectigo API Documentation. + Additional request fields are documented in Sectigo API Documentation + https://scm.devx.sectigo.com/reference/generate-ssl-certificates-log - Search fields for reports can be found in the Sectigo API Documentation. - Additional request fields are documented in Sectigo API Documentation - https://sectigo.com/faqs/detail/Sectigo-Certificate-Manager-SCM-REST-API/kA01N000000XDkE - - return dict: The report data + Returns: + dict: The report data """ data = {} for key, value in kwargs.items(): @@ -57,15 +59,19 @@ def get(self, report_name, **kwargs): def get_ssl_certs(self, **kwargs): """Get the specific SSL Certificate report. - The API provides several search fields commonly used available through kwargs: + Args: + kwargs: Additional fields that will be passed to the API + The API provides several search fields commonly used available through kwargs: "from": ISO date of start of date range "to": ISO date of start of date range "certificateDateAttribute": Date type(number) to search on: 2=revocation,3=expiration,4=requested,5=issuance "certificateStatus": Cert status(number): 0=Any,1=Requested,2=Issued,3=Revoked,4=Expired "organizationIds": Array of unique Org IDs to fiter search - Other fields: certificateRequestSource, serialNumberFormat, externalRequester + Other fields: certificateRequestSource, serialNumberFormat, externalRequester + https://scm.devx.sectigo.com/reference/generate-ssl-certificates-log - return dict: The report data + Returns: + dict: The report data """ report_url = "ssl-certificates" @@ -76,15 +82,19 @@ def get_ssl_certs(self, **kwargs): def get_client_certs(self, **kwargs): """Get the specific Client Certificate report. - The API provides several search fields commonly used available through kwargs: + Args: + kwargs: Additional fields that will be passed to the API + The API provides several search fields commonly used available through kwargs: "from": ISO date of start of date range "to": ISO date of start of date range "certificateDateAttribute": Date type(number) to search on: 0=entrolled, 1=downloaded,2=revocation,3=expired "certificateStatus": Cert status(number): 0=Any,2=Enrolled-downloaded,3=Revoked,4=Expired, 5=Enrolled-Pending_download,6=not_enrolled "organizationIds": Array of unique Org IDs to fiter search + https://scm.devx.sectigo.com/reference/generate-smime-certificates-log - return dict: The report data + Returns: + dict: The report data """ report_url = "client-certificates" @@ -93,15 +103,19 @@ def get_client_certs(self, **kwargs): def get_device_certs(self, **kwargs): """Get the specific Device Certificate report. - The API provides several search fields commonly used available through kwargs: + Args: + kwargs: Additional fields that will be passed to the API + The API provides several search fields commonly used available through kwargs: "from": ISO date of start of date range "to": ISO date of start of date range "certificateDateAttribute": Date type(number) to search on: 2=revocation,3=expired,4=requested,5=issuance "certificateStatus": Cert status(number): 0=Any,2=Enrolled-downloaded,,3=Revoked,4=Expired, 5=Enrolled-Pending_download, 6=not_enrolled "organizationIds": Array of unique Org IDs to fiter search + https://scm.devx.sectigo.com/reference/generate-device-certificates-log - return dict: The report data + Returns: + dict: The report data """ report_url = "device-certificates" @@ -110,11 +124,15 @@ def get_device_certs(self, **kwargs): def get_activity(self, **kwargs): """Get the specific Activity report. - The API provides 2 search fields to filter date range through kwargs: + Args: + kwargs: Additional fields that will be passed to the API + The API provides 2 search fields to filter date range through kwargs: "from": ISO date of start of date range "to": ISO date of start of date range + https://scm.devx.sectigo.com/reference/generate-activity-report - return dict: The report data + Returns: + dict: The report data """ report_url = "activity" @@ -123,7 +141,8 @@ def get_activity(self, **kwargs): def get_domains(self): """Get the specific Domains report. - return dict: The report data + Returns: + dict: The report data """ report_url = "domains" diff --git a/cert_manager/smime.py b/cert_manager/smime.py index 0aab7dd..fad62e9 100644 --- a/cert_manager/smime.py +++ b/cert_manager/smime.py @@ -15,11 +15,12 @@ class SMIME(Certificates): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/smime", api_version=api_version) - self.__reason_maxlen = 512 + self._reason_maxlen = 512 @paginate def list(self, **kwargs): @@ -30,9 +31,11 @@ def list(self, **kwargs): referenced at: https://sectigo.com/knowledge-base/detail/SCM-Sectigo-Certificate-Manager-REST-API/kA01N000000XDkE - :param dict kwargs: A dictionary of arguments to pass to the API + Args: + kwargs: A dictionary of arguments to pass to the API - :return iter: An iterator object is returned to cycle through the certificates + Returns: + iter: An iterator object is returned to cycle through the certificates """ result = self._client.get(self._api_url, params=kwargs) return result.json() @@ -41,8 +44,11 @@ def list(self, **kwargs): def list_by_email(self, **kwargs): """Return a list of all client certificates for a person with given email. - :param str email: Person email - :return iter: An iterator object is returned to cycle through the certificates + Args: + kwargs: A dictionary of arguments to pass to the API + + Returns: + iter: An iterator object is returned to cycle through the certificates """ email = kwargs["email"] @@ -52,24 +58,29 @@ def list_by_email(self, **kwargs): def enroll(self, **kwargs): """Enroll a client certificate request with Sectigo to generate a certificate. - :param string cert_type_name: The full cert type name - Note: the name must match names returned from the get_types() method - :param string csr: The Certificate Signing Request (CSR) - :param string email: The person's e-mail - :param string phone: The person's phone number - :param list secondary_emails: The person's secondary e-mail(s) - :param string first_name: The person's first name - :param string middleName: The person's middle name - :param string last_name: The person's last name - :param string common_name: The person's common name. - If ommited, constructed from person's full name - :param string eppn: The person's EPPN - :param string upn: The person's UPN (User Principal Name) - :param int term: The length, in days, for the certificate to be issued - :param int org_id: The ID of the organization in which to enroll the certificate - :param list custom_fields: zero or more objects representing custom fields and their values - Note: each object must have a 'name' key and a 'value' key - :return dict: The orderNumber (Obsolete, backendCertId should be used instead) and backendCertId + Args: + kwargs: A dictionary of arguments to pass to the API. + Allowed fields are: + cert_type_name: The full cert type name + Note: the name must match names returned from the get_types() method + csr: The Certificate Signing Request (CSR) + email: The person's e-mail + phone: The person's phone number + secondary_emails: The person's secondary e-mail(s) + first_name: The person's first name + middleName: The person's middle name + last_name: The person's last name + common_name: The person's common name. + If ommited, constructed from person's full name + eppn: The person's EPPN + upn: The person's UPN (User Principal Name) + term: The length, in days, for the certificate to be issued + org_id: The ID of the organization in which to enroll the certificate + custom_fields: zero or more objects representing custom fields and their values + Note: each object must have a 'name' key and a 'value' key + + Returns: + The orderNumber (Obsolete, backendCertId should be used instead) and backendCertId """ # Retrieve all the arguments cert_type_name = kwargs.get("cert_type_name") @@ -121,8 +132,11 @@ def collect(self, cert_id): This method will raise a PendingError exception if the certificate is still in a pending state. - :param int cert_id: The Certificate ID given on enroll success - :return str: the string representing the certificate in the requested format + Args: + cert_id: The Certificate ID given on enroll success + + Returns: + The string representing the certificate in the requested format """ if not cert_id: raise ValueError("Argument 'cert_id' can't be None") @@ -146,10 +160,13 @@ def collect(self, cert_id): def replace(self, **kwargs): """Replace a pre-existing client certificate. - :param int cert_id: The certificate ID - :param string csr: The Certificate Signing Request (CSR) - :param str reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist. - :param bool revoke: Revoke previous certificate if true. Default is True + Args: + kwargs: A dictionary of arguments to pass to the API. + Allowed fields are: + cert_id: The certificate ID + csr: The Certificate Signing Request (CSR) + reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist. + revoke: Revoke previous certificate if true. Default is True """ # Retrieve all the arguments cert_id = kwargs["cert_id"] @@ -161,15 +178,19 @@ def replace(self, **kwargs): data = {"csr": csr, "reason": reason, "revoke": revoke} self._client.post(url, data=data) + return {} + @version_hack(service="smime", version="v2") def renew(self, order_num="", serial_num=""): """Renew a client certificate with the specified order or serial number. - :param int order_num: The certificate order number - :param str serial_num: The certificate serial number - You can provide either the order or serial number, not both. + Args: + order_num: The certificate order number + serial_num: The certificate serial number + You can provide either the order or serial number, not both. - :return dict: A dictionary containing the new order number and cert ID + Returns: + dict: A dictionary containing the new order number and cert ID """ if order_num and serial_num: raise ValueError("Cannot provide both order number and serial number") @@ -185,9 +206,10 @@ def renew(self, order_num="", serial_num=""): def revoke(self, cert_id, reason=""): """Revoke a client certificate specified by the certificate ID. - :param int cert_id: The certificate ID - :param str reason: The Reason for revocation. - Reason can be up to 512 characters and cannot be blank (i.e. empty string) + Args: + cert_id: The certificate ID + reason: The Reason for revocation. + Reason can be up to 512 characters and cannot be blank (i.e. empty string) """ url = self._url(f"/revoke/order/{cert_id}") @@ -195,18 +217,21 @@ def revoke(self, cert_id, reason=""): raise ValueError("Argument 'cert_id' can't be None") # Sectigo has a 512 character limit on the "reason" message, so catch that here. - if not reason or len(reason) >= self.__reason_maxlen: - raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self.__reason_maxlen} characters") + if not reason or len(reason) >= self._reason_maxlen: + raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self._reason_maxlen} characters") data = {"reason": reason} self._client.post(url, data=data) + return {} + def revoke_by_email(self, email, reason=""): """Revoke all client certificate related to an email. - :param str email: The person email address - :param str reason: The Reason for revocation. - Reason can be up to 512 characters and cannot be blank (i.e. empty string) + Args: + email: The person email address + reason: The Reason for revocation. + Reason can be up to 512 characters and cannot be blank (i.e. empty string) """ url = self._url("/revoke") @@ -214,8 +239,10 @@ def revoke_by_email(self, email, reason=""): raise ValueError("Argument 'email' can't be empty or None") # Sectigo has a 512 character limit on the "reason" message, so catch that here. - if not reason or len(reason) >= self.__reason_maxlen: - raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self.__reason_maxlen} characters") + if not reason or len(reason) >= self._reason_maxlen: + raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self._reason_maxlen} characters") data = {"email": email, "reason": reason} self._client.post(url, data=data) + + return {} diff --git a/cert_manager/ssl.py b/cert_manager/ssl.py index 4769548..8da4200 100644 --- a/cert_manager/ssl.py +++ b/cert_manager/ssl.py @@ -14,8 +14,9 @@ class SSL(Certificates): def __init__(self, client, api_version="v1"): """Initialize the class. - :param object client: An instantiated cert_manager.Client object - :param string api_version: The API version to use; the default is "v1" + Args: + client: An instantiated cert_manager.Client object + api_version: The API version to use; the default is "v1" """ super().__init__(client=client, endpoint="/ssl", api_version=api_version) @@ -28,16 +29,25 @@ def list(self, **kwargs): referenced at: https://sectigo.com/uploads/audio/Certificate-Manager-20.1-Rest-API.html#resource-SSL-list - :param dict kwargs: A dictionary of arguments to pass to the API + Args: + kwargs: A dictionary of arguments to pass to the API - :return iter: An iterator object is returned to cycle through the certificates + Returns: + iter: An iterator object is returned to cycle through the certificates """ result = self._client.get(self._api_url, params=kwargs) return result.json() def get(self, cert_id): - """Retrieve a certificate corresponding to the given certificate ID.""" + """Retrieve a certificate corresponding to the given certificate ID. + + Args: + cert_id: The Certificate ID given on enroll success + + Returns: + The certificate details returned by the API + """ url = self._url(f"/{cert_id}") result = self._client.get(url) @@ -46,8 +56,11 @@ def get(self, cert_id): def renew(self, cert_id): """Renew the certificate specified by the certificate ID. - :param int cert_id: The certificate ID - :return dict: The renewal result. "Successful" on success + Args: + cert_id: The certificate ID + + Returns: + dict: The renewal result. "Successful" on success """ url = self._url(f"/renewById/{cert_id}") result = self._client.post(url, data="") @@ -56,7 +69,14 @@ def renew(self, cert_id): return {} def count(self, **kwargs) -> int: - """Retrieve the number of certifictes.""" + """Retrieve the number of certifictes. + + Args: + kwargs: A dictionary of arguments to pass to the API + + Returns: + The number of certificates + """ result = self._client.head(self._api_url, params=kwargs) return int(result.headers['X-Total-Count']) diff --git a/docs/README.md b/docs/README.md index 1f386d8..9cfe7c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,36 +16,52 @@ There are many API endpoints under Certificate Manager, and this library currently supports a subset of those endpoints. The current list of written and tested endpoint classes includes: -- Organization (/organization) -- Person (/person) -- SSL (/ssl) -- Client Administrator (/admin) -- Domain (/domain) -- Report (/report) - -Other endpoints we hope to add in the near future: - -- Code Signing Certificates (/csod) -- Custom Fields (/customField) +- Public ACME Accounts (/acme/v2/accounts) +- Administrators (/admin) - Domain Control Validation (/dcv) -- Device Certificates (/device) -- Discovery (/discovery) -- SMIME (/smime) +- Domain (/domain) +- Organization (/organization) +- Persons (/person) +- Reports (/report) +- Client Certificates (/smime) +- SSL Certificates (/ssl) ## Installing You can use pip to install cert_manager: -```Shell +```shell pip install cert_manager ``` +## Authentication + +Originally, Certificate Manager only allowed username and password, or client +certificate and key as methods of authenticating to the REST API. However, +OAuth2 is now supported via a completely different URL structure. This new model +can be used by setting the `client_id` and `client_secret` parameters to the +`Client` constructor. The Client ID and Client Secret are created via the UI in +[Sectigo][2]. Information on how to create the Client ID and Client Secret can +be found on +[How Do You Implement OAuth 2.0 for SCM](https://www.sectigo.com/knowledge-base/detail/implement-oauth-2-0-for-scm) +page. + +**NOTE** When using OAuth2 authentication with the new API URLs, pay particular +attention to the version of the API you are using for each class. Typically the +version is `v1`, but there isn't a consistent version across all endpoints. You +may need to add `api_version` to many of the object instantiations for the API +to work correctly. A complete API reference can be found on the +[SCM DevX](https://scm.devx.sectigo.com/reference/) site. This, for example, is +the first code snippet in [Examples](#examples) rewritten for the new API +infrastructure: + ## Examples -This is a simple example that just shows initializing the `Client` object and -using it to query the `Organization` and `SSL` endpoints: +This is a simple example that just shows initializing the `Client` object to use +username and password authentication and then using it to query the +`Organization` and `SSL` endpoints: -```Python +```python from cert_manager import Organization from cert_manager import Client from cert_manager import SSL @@ -64,10 +80,30 @@ print(ssl.types) print(org.all()) ``` +The same basic code can be used with OAuth2 authentication parameters. + +```python +from cert_manager import Organization +from cert_manager import Client +from cert_manager import SSL + +client = Client( + client_id="client-id-from-sectigo", + client_secret="client-secret-from-sectigo", +) + +org = Organization(client=client) +ssl = SSL(client=client, api_version="v2") + +print(ssl.types) +print(org.all()) +``` + The most common process you would do, however, is enroll and then collect a -certificate you want to order from the Certificate Manager: +certificate you want to order from the Certificate Manager. This example uses +the parameters to authenticate using certificate (client) authentication: -```Python +```python from time import sleep from cert_manager import Organization @@ -119,78 +155,59 @@ the CONTRIBUTING.md for specifics on contributions. We try to have a high level of test coverage on the code. Therefore, when adding anything to the repo, tests should be written to test a new feature or to test a bug fix so that there won't be a regression. This library is setup to be pretty -simple to build a working development environment using [Docker][4]. Therefore, -it is suggested that you have [Docker][4] installed where you clone this -repository to make development easier. - -To start a development environment, you should be able to just run the -`dev.bash` script. This script will use the `Containerfile` in this repository -to build a [Docker][4] container with all the dependencies for development +simple to build a working development environment using [Docker][4], +[Podman][7], or [Mise][8]. Therefore, it is suggested that you have [Docker][4] +[Podman][7], or [Mise][8] installed where you clone this repository to make +development easier. + +To start a containerized development environment, you should be able to just run +the `dev.bash` script. This script will use the `Containerfile` in this +repository to build a container image with all the dependencies for development installed using [Poetry][3]. -```Shell +```shell ./dev.bash ``` -The first time you run the script, it should build the [Docker][4] image and -then drop you into the container's shell. The directory where you cloned this +The first time you run the script, it should build the container image and then +drop you into the container's shell. The directory where you cloned this repository should be volume mounted in to `/working`, which should also be the current working directory. From there, you can make changes as you see fit. Tests can be run from the `/working` directory by simply typing `pytest` as [pytest][5] has been setup to with the correct parameters. -## Changelog +If you want to use [Mise][8], you should be able to set up the environment +simply with: -To generate the `CHANGELOG.md`, you will need [Docker][4] and a GitHub personal -access token. We currently use -[github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator) -for this purpose. The following should generate the file using information from -GitHub: - -```Shell -docker run -it --rm \ - -e CHANGELOG_GITHUB_TOKEN='yourtokenhere' \ - -v "$(pwd)":/working \ - -w /working \ - ferrarimarco/github-changelog-generator --verbose +```shell +mise install ``` -To generate the log for an upcoming release that has not yet been tagged, you -can run a command to include the upcoming release version. For example, `2.0.0`: - -```Shell -docker run -it --rm \ - -e CHANGELOG_GITHUB_TOKEN='yourtokenhere' \ - -v "$(pwd)":/working \ - -w /working \ - ferrarimarco/github-changelog-generator --verbose --future-release 2.0.0 --unreleased -``` +## Changelog -As a note, this repository uses the default labels for formatting the -`CHANGELOG.md`. Label information can be found here: -[Advanced-change-log-generation-examples](https://github.com/github-changelog-generator/github-changelog-generator/wiki/Advanced-change-log-generation-examples#section-options) +Going forward, the Changelog will be visible in the GitHub releases for this +repository. Changes for before version 3.0.0 can be found in the +[CHANGELOG.md](CHANGELOG.md) file within this repository. ## Releases -Releases to the codebase are typically done using the [bump2version][6] tool. -This tool takes care of updating the version in all necessary files, updating -its own configuration, and making a GitHub commit and tag. We typically do -version bumps as part of a PR, so you don't want to have [bump2version][6] tag -the version at the same time it does the commit as commit hashes may change. -Therefore, to bump the version a patch level, one would run the command: +Version updates to the codebase are typically done using the [bump2version][6] +tool. This tool takes care of updating the version in all necessary files and +updating its own configuration. To bump the version a patch level, one would run +the command: -```Shell -bump2version --verbose --no-tag patch +```shell +bump2version --verbose --no-commit --no-tag patch ``` -Once the PR is merged, you can then checkout the new `main` branch and tag it -using the new version number that is now in `.bumpversion.cfg`: +Releases are now done through the GitHub +[Release](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) +system. The easiest way to create a new release draft is using the GitHub CLI +(`gh`). For example, to create a new draft release for version `3.0.0` with +autogenerated notes: -```Shell -git checkout main -git pull --rebase -git tag 1.0.0 -m 'Bump version: 0.1.0 → 1.0.0' -git push --tags +```shell +gh release create 3.0.0 --draft --generate-notes --title 3.0.0 ``` [1]: https://www.python.org/ "Python" @@ -199,3 +216,5 @@ git push --tags [4]: https://www.docker.com/ "Docker" [5]: https://docs.pytest.org/en/stable/ "pytest" [6]: https://pypi.org/project/bump2version/ "bump2version" +[7]: https://podman.io/ "Podman" +[8]: https://mise.jdx.dev/ "Mise" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..29d6e81 --- /dev/null +++ b/mise.toml @@ -0,0 +1,5 @@ +[tools] +"aqua:j178/prek" = "latest" +"npm:prettier" = "latest" +node = "latest" +python = "3.13" diff --git a/poetry.lock b/poetry.lock index 55bf0ff..b3b22da 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "bump2version" @@ -14,116 +14,137 @@ files = [ [[package]] name = "certifi" -version = "2024.12.14" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"}, + {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, + {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, ] [[package]] @@ -258,17 +279,20 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -291,14 +315,14 @@ test = ["testtools"] [[package]] name = "idna" -version = "3.10" +version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [package.extras] @@ -306,14 +330,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -335,54 +359,60 @@ test = ["pytest", "pytest-cov"] [[package]] name = "packaging" -version = "24.2" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -414,65 +444,85 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] [[package]] @@ -519,25 +569,25 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy [[package]] name = "setuptools" -version = "78.1.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version >= \"3.12\"" files = [ - {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, - {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "testtools" @@ -573,45 +623,73 @@ files = [ [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 97f345e..eea52a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" +requires = ["poetry_core>=2"] [project] authors = [ @@ -13,7 +13,7 @@ license = { text = "BSD-3-Clause" } name = "cert_manager" readme = "docs/README.md" requires-python = ">=3.9,<4.0.0" -version = "2.4.0" +version = "3.0.0" [project.urls] homepage = "https://github.com/broadinstitute/python-cert_manager.git" @@ -28,10 +28,10 @@ bump2version = "^1.0.1" coverage = "^7.10.7" fixtures = "^4.2.6" mock = "^5.2.0" +pytest = "^8.4.2" responses = "^0.26.0" testtools = "^2.7.2" yamllint = "^1.37.1" -pytest = "^8.4.2" [tool.ruff] line-length = 120 diff --git a/tests/lib/testbase.py b/tests/lib/testbase.py index 699b008..5233f39 100644 --- a/tests/lib/testbase.py +++ b/tests/lib/testbase.py @@ -23,6 +23,8 @@ def _setUp(self): # noqa: N802 self.password = "test_password" self.user_crt_file = "/path/to/pub.key" self.user_key_file = "/path/to/priv.key" + self.client_id = "test_client_id" + self.client_secret = "test_client_secret" # This is basically the same code as the code used in Client. This is used just to lock in the # data that the user-agent should have in it. diff --git a/tests/test_admin.py b/tests/test_admin.py index 94200a9..8856029 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -73,7 +73,7 @@ def test_defaults(self): self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.api_url) - self.assertEqual(admin._Admin__admins, self.valid_response) + self.assertEqual(admin._admins, self.valid_response) @responses.activate def test_param(self): @@ -91,7 +91,7 @@ def test_param(self): self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, api_url) - self.assertEqual(admin._Admin__admins, self.valid_response) + self.assertEqual(admin._admins, self.valid_response) def test_need_client(self): """Raise an exception if called without a client parameter.""" diff --git a/tests/test_client.py b/tests/test_client.py index 2833668..3d2ba9d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,6 +10,7 @@ import sys from unittest import mock +import pytest import responses from requests.exceptions import HTTPError from testtools import TestCase @@ -57,20 +58,20 @@ def test_defaults(self): # Use the hackity object mangling when dealing with double-underscore values in an object # This hard-coded test is to test that the default base_url is used when none is provided - self.assertEqual(client._Client__base_url, "https://cert-manager.com/api") - self.assertEqual(client._Client__login_uri, self.cfixt.login_uri) - self.assertEqual(client._Client__username, self.cfixt.username) - self.assertEqual(client._Client__password, self.cfixt.password) - self.assertEqual(client._Client__cert_auth, False) + self.assertEqual(client._base_url, "https://cert-manager.com/api") + self.assertEqual(client._login_uri, self.cfixt.login_uri) + self.assertEqual(client._username, self.cfixt.username) + self.assertEqual(client._password, self.cfixt.password) + self.assertEqual(client._cert_auth, False) # Make sure all the headers make their way into the internal requests.Session object for head, headdata in self.cfixt.headers.items(): - self.assertTrue(head in client._Client__session.headers) - self.assertEqual(client._Client__session.headers[head], headdata) + self.assertTrue(head in client._session.headers) + self.assertEqual(client._session.headers[head], headdata) # Because password was used and cert_auth was False, a password header should exist - self.assertTrue("password" in client._Client__session.headers) - self.assertEqual(self.cfixt.password, client._Client__session.headers["password"]) + self.assertTrue("password" in client._session.headers) + self.assertEqual(self.cfixt.password, client._session.headers["password"]) def test_params(self): """Set parameters correctly inside the class using all parameters.""" @@ -81,21 +82,21 @@ def test_params(self): ) # Use the hackity object mangling when dealing with double-underscore values in an object - self.assertEqual(client._Client__base_url, self.cfixt.base_url) - self.assertEqual(client._Client__login_uri, self.cfixt.login_uri) - self.assertEqual(client._Client__username, self.cfixt.username) - self.assertEqual(client._Client__cert_auth, True) - self.assertEqual(client._Client__user_crt_file, self.cfixt.user_crt_file) - self.assertEqual(client._Client__user_key_file, self.cfixt.user_key_file) - self.assertEqual(client._Client__session.cert, (self.cfixt.user_crt_file, self.cfixt.user_key_file)) + self.assertEqual(client._base_url, self.cfixt.base_url) + self.assertEqual(client._login_uri, self.cfixt.login_uri) + self.assertEqual(client._username, self.cfixt.username) + self.assertEqual(client._cert_auth, True) + self.assertEqual(client._user_crt_file, self.cfixt.user_crt_file) + self.assertEqual(client._user_key_file, self.cfixt.user_key_file) + self.assertEqual(client._session.cert, (self.cfixt.user_crt_file, self.cfixt.user_key_file)) # Make sure all the headers make their way into the internal requests.Session object for head, headdata in self.cfixt.headers.items(): - self.assertTrue(head in client._Client__session.headers) - self.assertEqual(client._Client__session.headers[head], headdata) + self.assertTrue(head in client._session.headers) + self.assertEqual(client._session.headers[head], headdata) # If cert_auth is True, make sure a password header does not exist - self.assertFalse("password" in client._Client__session.headers) + self.assertFalse("password" in client._session.headers) def test_no_pass_with_certs(self): """Set parameters correctly inside the class certificate auth without a password.""" @@ -105,21 +106,21 @@ def test_no_pass_with_certs(self): ) # Use the hackity object mangling when dealing with double-underscore values in an object - self.assertEqual(client._Client__base_url, self.cfixt.base_url) - self.assertEqual(client._Client__login_uri, self.cfixt.login_uri) - self.assertEqual(client._Client__username, self.cfixt.username) - self.assertEqual(client._Client__cert_auth, True) - self.assertEqual(client._Client__user_crt_file, self.cfixt.user_crt_file) - self.assertEqual(client._Client__user_key_file, self.cfixt.user_key_file) - self.assertEqual(client._Client__session.cert, (self.cfixt.user_crt_file, self.cfixt.user_key_file)) + self.assertEqual(client._base_url, self.cfixt.base_url) + self.assertEqual(client._login_uri, self.cfixt.login_uri) + self.assertEqual(client._username, self.cfixt.username) + self.assertEqual(client._cert_auth, True) + self.assertEqual(client._user_crt_file, self.cfixt.user_crt_file) + self.assertEqual(client._user_key_file, self.cfixt.user_key_file) + self.assertEqual(client._session.cert, (self.cfixt.user_crt_file, self.cfixt.user_key_file)) # Make sure all the headers make their way into the internal requests.Session object for head, headdata in self.cfixt.headers.items(): - self.assertTrue(head in client._Client__session.headers) - self.assertEqual(client._Client__session.headers[head], headdata) + self.assertTrue(head in client._session.headers) + self.assertEqual(client._session.headers[head], headdata) # If cert_auth is True, make sure a password header does not exist - self.assertFalse("password" in client._Client__session.headers) + self.assertFalse("password" in client._session.headers) def test_versioning(self): """Change the user-agent header if the version number changes.""" @@ -135,7 +136,7 @@ def test_versioning(self): # Make sure the user-agent header is correct in the class and the internal requests.Session object self.assertEqual(client.headers["User-Agent"], user_agent) - self.assertEqual(client._Client__session.headers["User-Agent"], user_agent) + self.assertEqual(client._session.headers["User-Agent"], user_agent) def test_need_crt(self): """Raise an exception without a cert file if cert_auth=True.""" @@ -173,6 +174,75 @@ def test_need_private_key(self): password=self.cfixt.password, cert_auth=True, user_crt_file=self.cfixt.user_crt_file, ) + def test_oauth2_defaults(self): + """Set OAuth2 defaults and add bearer token authorization header.""" + token = "abc123" + client_id = "test_client_id" + client_secret = "test_client_secret" + post_response = mock.Mock() + post_response.json.return_value = {"access_token": token} + + with mock.patch("cert_manager.client.requests.post", return_value=post_response) as post_mock: + client = Client(client_id=client_id, client_secret=client_secret) + + self.assertEqual(client._base_url, "https://admin.enterprise.sectigo.com/api") + self.assertEqual(client._headers, {"Authorization": f"Bearer {token}"}) + self.assertEqual(client._session.headers["Authorization"], f"Bearer {token}") + + post_mock.assert_called_once_with( + "https://auth.sso.sectigo.com/auth/realms/apiclients/protocol/openid-connect/token", + data={ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + }, + headers={"accept": "application/json", "content-type": "application/x-www-form-urlencoded"}, + ) + + def test_oauth2_params(self): + """Use provided OAuth2 URLs for auth and API base URL.""" + token = "xyz987" + client_id = "test_client_id" + client_secret = "test_client_secret" + auth_url = "https://auth.example.com/realms/custom/protocol/openid-connect/token" + base_url = "https://api.example.com/custom" + post_response = mock.Mock() + post_response.json.return_value = {"access_token": token} + + with mock.patch("cert_manager.client.requests.post", return_value=post_response) as post_mock: + client = Client( + client_id=client_id, + client_secret=client_secret, + auth_url=auth_url, + base_url=base_url, + ) + + self.assertEqual(client._base_url, base_url) + self.assertEqual(client._session.headers["Authorization"], f"Bearer {token}") + self.assertFalse("password" in client._session.headers) + self.assertFalse("login" in client._session.headers) + self.assertFalse("customerUri" in client._session.headers) + + post_mock.assert_called_once_with( + auth_url, + data={ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + }, + headers={"accept": "application/json", "content-type": "application/x-www-form-urlencoded"}, + ) + + def test_need_client_secret_for_oauth2(self): + """Raise an exception without client_secret when client_id is provided.""" + with pytest.raises(KeyError): + Client(client_id="test_client_id") + + def test_need_client_id_for_oauth2(self): + """Raise an exception without client_id when client_secret is provided.""" + with pytest.raises(KeyError): + Client(client_secret="test_client_secret") + class TestProperties(TestClient): """Test the property methods in the class.""" @@ -193,7 +263,7 @@ def test_headers(self): def test_session(self): """The session property should return the correct value.""" - self.assertEqual(self.client._Client__session, self.client.session) + self.assertEqual(self.client._session, self.client.session) class TestAddHeaders(TestClient): @@ -207,13 +277,13 @@ def test_add(self): # Make sure the new headers make their way into the internal requests.Session object for header, hval in headers.items(): - self.assertTrue(header in self.client._Client__session.headers) - self.assertEqual(hval, self.client._Client__session.headers[header]) + self.assertTrue(header in self.client._session.headers) + self.assertEqual(hval, self.client._session.headers[header]) # Make sure the original headers are still in the internal requests.Session object for head, headdata in self.cfixt.headers.items(): - self.assertTrue(head in self.client._Client__session.headers) - self.assertEqual(self.client._Client__session.headers[head], headdata) + self.assertTrue(head in self.client._session.headers) + self.assertEqual(self.client._session.headers[head], headdata) def test_replace(self): """The already existing header should be modified.""" @@ -223,15 +293,15 @@ def test_replace(self): # Make sure the new headers make their way into the internal requests.Session object for header, hval in headers.items(): - self.assertTrue(header in self.client._Client__session.headers) - self.assertEqual(hval, self.client._Client__session.headers[header]) + self.assertTrue(header in self.client._session.headers) + self.assertEqual(hval, self.client._session.headers[header]) # Removed the modified header from the check as it was checked above del self.cfixt.headers["User-Agent"] # Make sure the original headers are still in the internal requests.Session object for head, headdata in self.cfixt.headers.items(): - self.assertTrue(head in self.client._Client__session.headers) - self.assertEqual(self.client._Client__session.headers[head], headdata) + self.assertTrue(head in self.client._session.headers) + self.assertEqual(self.client._session.headers[head], headdata) def test_not_dictionary(self): """Raise an exception when not passed a dictionary.""" @@ -250,13 +320,13 @@ def test_remove(self): # Make sure the headers are removed from the requests.Session object for head in headers: - self.assertFalse(head in self.client._Client__session.headers) + self.assertFalse(head in self.client._session.headers) # Make sure the rest of the headers we added before are still there for head, headdata in self.cfixt.headers.items(): if head not in headers: - self.assertTrue(head in self.client._Client__session.headers) - self.assertEqual(self.client._Client__session.headers[head], headdata) + self.assertTrue(head in self.client._session.headers) + self.assertEqual(self.client._session.headers[head], headdata) def test_dictionary(self): """Remove headers correctly if passed a dictionary.""" @@ -266,13 +336,13 @@ def test_dictionary(self): # Make sure the headers are removed from the requests.Session object for head in headers: - self.assertFalse(head in self.client._Client__session.headers) + self.assertFalse(head in self.client._session.headers) # Make sure the rest of the headers we added before are still there for head, headdata in self.cfixt.headers.items(): if head not in headers: - self.assertTrue(head in self.client._Client__session.headers) - self.assertEqual(self.client._Client__session.headers[head], headdata) + self.assertTrue(head in self.client._session.headers) + self.assertEqual(self.client._session.headers[head], headdata) class TestGet(TestClient): diff --git a/tests/test_organization.py b/tests/test_organization.py index d6dbc71..76f6135 100644 --- a/tests/test_organization.py +++ b/tests/test_organization.py @@ -57,7 +57,7 @@ def test_defaults(self): self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, self.api_url) - self.assertEqual(org._Organization__orgs, self.valid_response) + self.assertEqual(org._orgs, self.valid_response) @responses.activate def test_param(self): @@ -75,7 +75,7 @@ def test_param(self): self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].request.url, api_url) - self.assertEqual(org._Organization__orgs, self.valid_response) + self.assertEqual(org._orgs, self.valid_response) def test_need_client(self): """Raise an exception if called without a client parameter."""