├── LICENSE.txt ├── README.rst ├── certbot_dns_ispconfig ├── __init__.py ├── dns_ispconfig.py └── dns_ispconfig_test.py ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── setup.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Electronic Frontier Foundation and others 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | 190 | END OF TERMS AND CONDITIONS 191 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | certbot-dns-ispconfig 2 | ===================== 3 | 4 | ISPConfig_ DNS Authenticator plugin for Certbot 5 | 6 | This plugin automates the process of completing a ``dns-01`` challenge by 7 | creating, and subsequently removing, TXT records using the ISPConfig Remote API. 8 | 9 | Configuration of ISPConfig 10 | --------------------------- 11 | 12 | In the `System -> Remote Users` you have to have a user, with the following rights 13 | 14 | - Client Functions 15 | - DNS zone functions 16 | - DNS txt functions 17 | 18 | 19 | .. _ISPConfig: https://www.ispconfig.org/ 20 | .. _certbot: https://certbot.eff.org/ 21 | 22 | Installation 23 | ------------ 24 | 25 | :: 26 | 27 | pip install certbot-dns-ispconfig 28 | 29 | 30 | Named Arguments 31 | --------------- 32 | 33 | To start using DNS authentication for ispconfig, pass the following arguments on 34 | certbot's command line: 35 | 36 | ============================================================= ============================================== 37 | ``--authenticator certbot-dns-ispconfig:dns-ispconfig`` select the authenticator plugin (Required) 38 | 39 | ``--certbot-dns-ispconfig:dns-ispconfig-credentials`` ispconfig Remote User credentials 40 | INI file. (Required) 41 | 42 | ``--certbot-dns-ispconfig:dns-ispconfig-propagation-seconds`` | waiting time for DNS to propagate before asking 43 | | the ACME server to verify the DNS record. 44 | | (Default: 120, Recommended: >= 600) 45 | ============================================================= ============================================== 46 | 47 | (Note that the verbose and seemingly redundant ``certbot-dns-ispconfig:`` prefix 48 | is currently imposed by certbot for external plugins.) 49 | 50 | 51 | Credentials 52 | ----------- 53 | 54 | An example ``credentials.ini`` file: 55 | 56 | .. code-block:: ini 57 | 58 | certbot_dns_ispconfig:dns_ispconfig_username = myremoteuser 59 | certbot_dns_ispconfig:dns_ispconfig_password = verysecureremoteuserpassword 60 | certbot_dns_ispconfig:dns_ispconfig_endpoint = https://you.ipsconfig.host:8080/remote/json.php 61 | 62 | The path to this file can be provided interactively or using the 63 | ``--certbot-dns-ispconfig:dns-ispconfig-credentials`` command-line argument. Certbot 64 | records the path to this file for use during renewal, but does not store the 65 | file's contents. 66 | 67 | **CAUTION:** You should protect these API credentials as you would the 68 | password to your ispconfig account. Users who can read this file can use these 69 | credentials to issue arbitrary API calls on your behalf. Users who can cause 70 | Certbot to run using these credentials can complete a ``dns-01`` challenge to 71 | acquire new certificates or revoke existing certificates for associated 72 | domains, even if those domains aren't being managed by this server. 73 | 74 | Certbot will emit a warning if it detects that the credentials file can be 75 | accessed by other users on your system. The warning reads "Unsafe permissions 76 | on credentials configuration file", followed by the path to the credentials 77 | file. This warning will be emitted each time Certbot uses the credentials file, 78 | including for renewal, and cannot be silenced except by addressing the issue 79 | (e.g., by using a command like ``chmod 600`` to restrict access to the file). 80 | 81 | 82 | Examples 83 | -------- 84 | 85 | To acquire a single certificate for both ``example.com`` and 86 | ``*.example.com``, waiting 900 seconds for DNS propagation: 87 | 88 | .. code-block:: bash 89 | 90 | certbot certonly \ 91 | --authenticator certbot-dns-ispconfig:dns-ispconfig \ 92 | --certbot-dns-ispconfig:dns-ispconfig-credentials /etc/letsencrypt/.secrets/domain.tld.ini \ 93 | --certbot-dns-ispconfig:dns-ispconfig-propagation-seconds 900 \ 94 | --server https://acme-v02.api.letsencrypt.org/directory \ 95 | --agree-tos \ 96 | --rsa-key-size 4096 \ 97 | -d 'example.com' \ 98 | -d '*.example.com' 99 | 100 | 101 | Docker 102 | ------ 103 | 104 | In order to create a docker container with a certbot-dns-ispconfig installation, 105 | create an empty directory with the following ``Dockerfile``: 106 | 107 | .. code-block:: docker 108 | 109 | FROM certbot/certbot 110 | RUN pip install certbot-dns-ispconfig 111 | 112 | Proceed to build the image:: 113 | 114 | docker build -t certbot/dns-ispconfig . 115 | 116 | Once that's finished, the application can be run as follows:: 117 | 118 | docker run --rm \ 119 | -v /var/lib/letsencrypt:/var/lib/letsencrypt \ 120 | -v /etc/letsencrypt:/etc/letsencrypt \ 121 | --cap-drop=all \ 122 | certbot/dns-ispconfig certonly \ 123 | --authenticator certbot-dns-ispconfig:dns-ispconfig \ 124 | --certbot-dns-ispconfig:dns-ispconfig-propagation-seconds 900 \ 125 | --certbot-dns-ispconfig:dns-ispconfig-credentials \ 126 | /etc/letsencrypt/.secrets/domain.tld.ini \ 127 | --no-self-upgrade \ 128 | --keep-until-expiring --non-interactive --expand \ 129 | --server https://acme-v02.api.letsencrypt.org/directory \ 130 | -d example.com -d '*.example.com' 131 | 132 | It is suggested to secure the folder as follows:: 133 | chown root:root /etc/letsencrypt/.secrets 134 | chmod 600 /etc/letsencrypt/.secrets 135 | -------------------------------------------------------------------------------- /certbot_dns_ispconfig/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `~certbot_dns_ispconfig.dns_ispconfig` plugin automates the process of 3 | completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and 4 | subsequently removing, TXT records using the ISPConfig REST API. 5 | 6 | 7 | Named Arguments 8 | --------------- 9 | 10 | ======================================== ===================================== 11 | ``--dns-ispconfig-credentials`` ISPConfig Remote API credentials_ 12 | INI file. (Required) 13 | ``--dns-ispconfig-propagation-seconds`` The number of seconds to wait for DNS 14 | to propagate before asking the ACME 15 | server to verify the DNS record. 16 | (Default: 120) 17 | ======================================== ===================================== 18 | 19 | 20 | Credentials 21 | ----------- 22 | 23 | Use of this plugin requires a configuration file containing ISPConfig Remote API 24 | credentials, obtained from your DNSimple 25 | `System > Remote Users`. 26 | 27 | .. code-block:: ini 28 | :name: credentials.ini 29 | :caption: Example credentials file: 30 | 31 | # ISPCONFIG API credentials used by Certbot 32 | dns_ispconfig_username = myispremoteuser 33 | dns_ispconfig_password = mysecretpassword 34 | dns_ispconfig_endpoint = https://localhost:8080 35 | 36 | The path to this file can be provided interactively or using the 37 | ``--dns-ispconfig-credentials`` command-line argument. Certbot records the path 38 | to this file for use during renewal, but does not store the file's contents. 39 | 40 | .. caution:: 41 | You should protect these API credentials as you would a password. Users who 42 | can read this file can use these credentials to issue arbitrary API calls on 43 | your behalf. Users who can cause Certbot to run using these credentials can 44 | complete a ``dns-01`` challenge to acquire new certificates or revoke 45 | existing certificates for associated domains, even if those domains aren't 46 | being managed by this server. 47 | 48 | Certbot will emit a warning if it detects that the credentials file can be 49 | accessed by other users on your system. The warning reads "Unsafe permissions 50 | on credentials configuration file", followed by the path to the credentials 51 | file. This warning will be emitted each time Certbot uses the credentials file, 52 | including for renewal, and cannot be silenced except by addressing the issue 53 | (e.g., by using a command like ``chmod 600`` to restrict access to the file). 54 | 55 | Examples 56 | -------- 57 | 58 | .. code-block:: bash 59 | :caption: To acquire a certificate for ``example.com`` 60 | 61 | certbot certonly \\ 62 | --dns-ispconfig \\ 63 | --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ 64 | -d example.com 65 | 66 | .. code-block:: bash 67 | :caption: To acquire a single certificate for both ``example.com`` and 68 | ``www.example.com`` 69 | 70 | certbot certonly \\ 71 | --dns-ispconfig \\ 72 | --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ 73 | -d example.com \\ 74 | -d www.example.com 75 | 76 | .. code-block:: bash 77 | :caption: To acquire a certificate for ``example.com``, waiting 240 seconds 78 | for DNS propagation 79 | 80 | certbot certonly \\ 81 | --dns-ispconfig \\ 82 | --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ 83 | --dns-ispconfig-propagation-seconds 240 \\ 84 | -d example.com 85 | 86 | """ 87 | -------------------------------------------------------------------------------- /certbot_dns_ispconfig/dns_ispconfig.py: -------------------------------------------------------------------------------- 1 | """DNS Authenticator for ISPConfig.""" 2 | import json 3 | import logging 4 | import time 5 | 6 | import requests 7 | import zope.interface 8 | 9 | from certbot import errors 10 | from certbot import interfaces 11 | from certbot.plugins import dns_common 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @zope.interface.implementer(interfaces.IAuthenticator) 17 | @zope.interface.provider(interfaces.IPluginFactory) 18 | class Authenticator(dns_common.DNSAuthenticator): 19 | """DNS Authenticator for ISPConfig 20 | 21 | This Authenticator uses the ISPConfig Remote REST API to fulfill a dns-01 challenge. 22 | """ 23 | 24 | description = "Obtain certificates using a DNS TXT record (if you are using ISPConfig for DNS)." 25 | ttl = 60 26 | 27 | def __init__(self, *args, **kwargs): 28 | super(Authenticator, self).__init__(*args, **kwargs) 29 | self.credentials = None 30 | 31 | @classmethod 32 | def add_parser_arguments(cls, add): # pylint: disable=arguments-differ 33 | super(Authenticator, cls).add_parser_arguments( 34 | add, default_propagation_seconds=120 35 | ) 36 | add("credentials", help="ISPConfig credentials INI file.") 37 | 38 | def more_info(self): # pylint: disable=missing-docstring,no-self-use 39 | return ( 40 | "This plugin configures a DNS TXT record to respond to a dns-01 challenge using " 41 | + "the ISPConfig Remote REST API." 42 | ) 43 | 44 | def _setup_credentials(self): 45 | self.credentials = self._configure_credentials( 46 | "credentials", 47 | "ISPConfig credentials INI file", 48 | { 49 | "endpoint": "URL of the ISPConfig Remote API.", 50 | "username": "Username for ISPConfig Remote API.", 51 | "password": "Password for ISPConfig Remote API.", 52 | }, 53 | ) 54 | 55 | def _perform(self, domain, validation_name, validation): 56 | self._get_ispconfig_client().add_txt_record( 57 | domain, validation_name, validation, self.ttl 58 | ) 59 | 60 | def _cleanup(self, domain, validation_name, validation): 61 | self._get_ispconfig_client().del_txt_record( 62 | domain, validation_name, validation, self.ttl 63 | ) 64 | 65 | def _get_ispconfig_client(self): 66 | return _ISPConfigClient( 67 | self.credentials.conf("endpoint"), 68 | self.credentials.conf("username"), 69 | self.credentials.conf("password"), 70 | ) 71 | 72 | 73 | class _ISPConfigClient(object): 74 | """ 75 | Encapsulates all communication with the ISPConfig Remote REST API. 76 | """ 77 | 78 | def __init__(self, endpoint, username, password): 79 | logger.debug("creating ispconfigclient") 80 | self.endpoint = endpoint 81 | self.username = username 82 | self.password = password 83 | self.session = requests.Session() 84 | self.session_id = None 85 | 86 | def _login(self): 87 | if self.session_id is not None: 88 | return 89 | logger.debug("logging in") 90 | logindata = {"username": self.username, "password": self.password} 91 | self.session_id = self._api_request("login", logindata) 92 | logger.debug("session id is %s", self.session_id) 93 | 94 | def _api_request(self, action, data): 95 | if self.session_id is not None: 96 | data["session_id"] = self.session_id 97 | url = self._get_url(action) 98 | resp = self.session.get(url, json=data) 99 | logger.debug("API REquest to URL: %s", url) 100 | if resp.status_code != 200: 101 | raise errors.PluginError( 102 | "HTTP Error during login {0}".format(resp.status_code) 103 | ) 104 | try: 105 | result = resp.json() 106 | except json.decoder.JSONDecodeError: 107 | raise errors.PluginError( 108 | "API response with non JSON: {0}".format(resp.text) 109 | ) 110 | if result["code"] == "ok": 111 | return result["response"] 112 | elif result["code"] == "remote_fault": 113 | raise errors.PluginError( 114 | "API response with an error: {0}".format(result["message"]) 115 | ) 116 | else: 117 | raise errors.PluginError("API response unknown {0}".format(resp.text)) 118 | 119 | def _get_url(self, action): 120 | return "{0}?{1}".format(self.endpoint, action) 121 | 122 | def _get_server_id(self, zone_id): 123 | zone = self._api_request("dns_zone_get", {"primary_id": zone_id}) 124 | return zone["server_id"] 125 | 126 | def add_txt_record(self, domain, record_name, record_content, record_ttl): 127 | """ 128 | Add a TXT record using the supplied information. 129 | 130 | :param str domain: The domain to use to look up the managed zone. 131 | :param str record_name: The record name (typically beginning with '_acme-challenge.'). 132 | :param str record_content: The record content (typically the challenge validation). 133 | :param int record_ttl: The record TTL (number of seconds that the record may be cached). 134 | :raises certbot.errors.PluginError: if an error occurs communicating with the ISPConfig API 135 | """ 136 | self._login() 137 | zone_id, zone_name = self._find_managed_zone_id(domain, record_name) 138 | if zone_id is None: 139 | raise errors.PluginError("Domain not known") 140 | logger.debug("domain found: %s with id: %s", zone_name, zone_id) 141 | o_record_name = record_name 142 | record_name = record_name.replace(zone_name, "")[:-1] 143 | logger.debug( 144 | "using record_name: %s from original: %s", record_name, o_record_name 145 | ) 146 | record = self.get_existing_txt(zone_id, record_name, record_content) 147 | if record is not None: 148 | if record["data"] == record_content: 149 | logger.info("already there, id {0}".format(record["id"])) 150 | return 151 | else: 152 | logger.info("update {0}".format(record["id"])) 153 | self._update_txt_record( 154 | zone_id, record["id"], record_name, record_content, record_ttl 155 | ) 156 | else: 157 | logger.info("insert new txt record") 158 | self._insert_txt_record(zone_id, record_name, record_content, record_ttl) 159 | 160 | def del_txt_record(self, domain, record_name, record_content, record_ttl): 161 | """ 162 | Delete a TXT record using the supplied information. 163 | 164 | :param str domain: The domain to use to look up the managed zone. 165 | :param str record_name: The record name (typically beginning with '_acme-challenge.'). 166 | :param str record_content: The record content (typically the challenge validation). 167 | :param int record_ttl: The record TTL (number of seconds that the record may be cached). 168 | :raises certbot.errors.PluginError: if an error occurs communicating with the ISPConfig API 169 | """ 170 | self._login() 171 | zone_id, zone_name = self._find_managed_zone_id(domain, record_name) 172 | if zone_id is None: 173 | raise errors.PluginError("Domain not known") 174 | logger.debug("domain found: %s with id: %s", zone_name, zone_id) 175 | o_record_name = record_name 176 | record_name = record_name.replace(zone_name, "")[:-1] 177 | logger.debug( 178 | "using record_name: %s from original: %s", record_name, o_record_name 179 | ) 180 | record = self.get_existing_txt(zone_id, record_name, record_content) 181 | if record is not None: 182 | if record["data"] == record_content: 183 | logger.debug("delete TXT record: %s", record["id"]) 184 | self._delete_txt_record(record["id"]) 185 | 186 | def _prepare_rr_data(self, zone_id, record_name, record_content, record_ttl): 187 | server_id = self._get_server_id(zone_id) 188 | data = { 189 | "client_id": None, 190 | "rr_type": "TXT", 191 | "params": { 192 | "server_id": server_id, 193 | "name": record_name, 194 | "active": "Y", 195 | "type": "TXT", 196 | "data": record_content, 197 | "zone": zone_id, 198 | "ttl": record_ttl, 199 | "update_serial": True, 200 | "stamp": time.strftime('%Y-%m-%d %H:%M:%S'), 201 | }, 202 | } 203 | return data 204 | 205 | def _insert_txt_record(self, zone_id, record_name, record_content, record_ttl): 206 | data = self._prepare_rr_data(zone_id, record_name, record_content, record_ttl) 207 | logger.debug("insert with data: %s", data) 208 | self._api_request("dns_txt_add", data) 209 | 210 | def _update_txt_record( 211 | self, zone_id, primary_id, record_name, record_content, record_ttl 212 | ): 213 | data = self._prepare_rr_data(zone_id, record_name, record_content, record_ttl) 214 | data["primary_id"] = primary_id 215 | logger.debug("update with data: %s", data) 216 | self._api_request("dns_txt_update", data) 217 | 218 | def _delete_txt_record(self, primary_id): 219 | data = {"primary_id": primary_id} 220 | logger.debug("delete with data: %s", data) 221 | self._api_request("dns_txt_delete", data) 222 | 223 | def _find_managed_zone_id(self, domain, record_name): 224 | """ 225 | Find the managed zone for a given domain. 226 | 227 | :param str domain: The domain for which to find the managed zone. 228 | :returns: The ID of the managed zone, if found. 229 | :rtype: str 230 | :raises certbot.errors.PluginError: if the managed zone cannot be found. 231 | """ 232 | 233 | zone_dns_name_guesses = [record_name] + dns_common.base_domain_name_guesses(domain) 234 | 235 | for zone_name in zone_dns_name_guesses: 236 | # get the zone id 237 | try: 238 | logger.debug("looking for zone: %s", zone_name) 239 | zone_id = self._api_request("dns_zone_get_id", {"origin": zone_name}) 240 | return zone_id, zone_name 241 | except errors.PluginError: 242 | pass 243 | return None, None 244 | 245 | def get_existing_txt(self, zone_id, record_name, record_content): 246 | """ 247 | Get existing TXT records from the RRset for the record name. 248 | 249 | If an error occurs while requesting the record set, it is suppressed 250 | and None is returned. 251 | 252 | :param str zone_id: The ID of the managed zone. 253 | :param str record_name: The record name (typically beginning with '_acme-challenge.'). 254 | 255 | :returns: TXT record value or None 256 | :rtype: `string` or `None` 257 | 258 | """ 259 | self._login() 260 | read_zone_data = {"zone_id": zone_id} 261 | zone_data = self._api_request("dns_rr_get_all_by_zone", read_zone_data) 262 | for entry in zone_data: 263 | if ( 264 | entry["name"] == record_name 265 | and entry["type"] == "TXT" 266 | and entry["data"] == record_content 267 | ): 268 | return entry 269 | return None 270 | -------------------------------------------------------------------------------- /certbot_dns_ispconfig/dns_ispconfig_test.py: -------------------------------------------------------------------------------- 1 | """Tests for certbot_dns_ispconfig.dns_ispconfig.""" 2 | 3 | import unittest 4 | 5 | import mock 6 | import json 7 | import requests_mock 8 | 9 | from certbot import errors 10 | from certbot.compat import os 11 | from certbot.plugins import dns_test_common 12 | from certbot.plugins.dns_test_common import DOMAIN 13 | from certbot.tests import util as test_util 14 | 15 | FAKE_USER = "remoteuser" 16 | FAKE_PW = "password" 17 | FAKE_ENDPOINT = "mock://endpoint" 18 | 19 | 20 | class AuthenticatorTest( 21 | test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest 22 | ): 23 | def setUp(self): 24 | super(AuthenticatorTest, self).setUp() 25 | 26 | from certbot_dns_ispconfig.dns_ispconfig import Authenticator 27 | 28 | path = os.path.join(self.tempdir, "file.ini") 29 | dns_test_common.write( 30 | { 31 | "ispconfig_username": FAKE_USER, 32 | "ispconfig_password": FAKE_PW, 33 | "ispconfig_endpoint": FAKE_ENDPOINT, 34 | }, 35 | path, 36 | ) 37 | 38 | super(AuthenticatorTest, self).setUp() 39 | self.config = mock.MagicMock( 40 | ispconfig_credentials=path, ispconfig_propagation_seconds=0 41 | ) # don't wait during tests 42 | 43 | self.auth = Authenticator(self.config, "ispconfig") 44 | 45 | self.mock_client = mock.MagicMock() 46 | # _get_ispconfig_client | pylint: disable=protected-access 47 | self.auth._get_ispconfig_client = mock.MagicMock(return_value=self.mock_client) 48 | 49 | def test_perform(self): 50 | self.auth.perform([self.achall]) 51 | 52 | expected = [ 53 | mock.call.add_txt_record( 54 | DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY, mock.ANY 55 | ) 56 | ] 57 | self.assertEqual(expected, self.mock_client.mock_calls) 58 | 59 | def test_cleanup(self): 60 | # _attempt_cleanup | pylint: disable=protected-access 61 | self.auth._attempt_cleanup = True 62 | self.auth.cleanup([self.achall]) 63 | 64 | expected = [ 65 | mock.call.del_txt_record( 66 | DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY, mock.ANY 67 | ) 68 | ] 69 | self.assertEqual(expected, self.mock_client.mock_calls) 70 | 71 | 72 | class ISPConfigClientTest(unittest.TestCase): 73 | record_name = "foo" 74 | record_content = "bar" 75 | record_ttl = 42 76 | 77 | def setUp(self): 78 | from certbot_dns_ispconfig.dns_ispconfig import _ISPConfigClient 79 | 80 | self.adapter = requests_mock.Adapter() 81 | 82 | self.client = _ISPConfigClient(FAKE_ENDPOINT, FAKE_USER, FAKE_PW) 83 | self.client.session.mount("mock", self.adapter) 84 | 85 | def _register_response( 86 | self, ep_id, response=None, message=None, additional_matcher=None, **kwargs 87 | ): 88 | resp = {"code": "ok", "message": message, "response": response} 89 | if message is not None: 90 | resp["code"] = "remote_failure" 91 | 92 | def add_matcher(request): 93 | data = json.loads(request.text) 94 | add_result = True 95 | if additional_matcher is not None: 96 | add_result = additional_matcher(request) 97 | 98 | return ( 99 | ( 100 | ("username" in data and data["username"] == FAKE_USER) 101 | and ("username" in data and data["password"] == FAKE_PW) 102 | ) 103 | or data["session_id"] == "FAKE_SESSION" 104 | ) and add_result 105 | 106 | self.adapter.register_uri( 107 | requests_mock.ANY, 108 | "{0}?{1}".format(FAKE_ENDPOINT, ep_id), 109 | text=json.dumps(resp), 110 | additional_matcher=add_matcher, 111 | **kwargs 112 | ) 113 | 114 | def test_add_txt_record(self): 115 | self._register_response("login", response="FAKE_SESSION") 116 | self._register_response("dns_zone_get_id", response=23) 117 | self._register_response("dns_txt_add", response=99) 118 | self._register_response( 119 | "dns_zone_get", response={"zone_id": 102, "server_id": 1} 120 | ) 121 | self._register_response("dns_rr_get_all_by_zone", response=[]) 122 | self.client.add_txt_record( 123 | DOMAIN, self.record_name, self.record_content, self.record_ttl 124 | ) 125 | 126 | def test_add_txt_record_fail_to_find_domain(self): 127 | self._register_response("login", response="FAKE_SESSION") 128 | self._register_response("dns_zone_get_id", message="Not Found") 129 | with self.assertRaises(errors.PluginError): 130 | self.client.add_txt_record( 131 | DOMAIN, self.record_name, self.record_content, self.record_ttl 132 | ) 133 | 134 | def test_add_txt_record_fail_to_authenticate(self): 135 | self._register_response("login", message="FAILED") 136 | with self.assertRaises(errors.PluginError): 137 | self.client.add_txt_record( 138 | DOMAIN, self.record_name, self.record_content, self.record_ttl 139 | ) 140 | 141 | def test_del_txt_record(self): 142 | self._register_response("login", response="FAKE_SESSION") 143 | self._register_response("dns_zone_get_id", response=23) 144 | self._register_response("dns_rr_get_all_by_zone", response=[]) 145 | self._register_response("dns_txt_delete", response="") 146 | self.client.del_txt_record( 147 | DOMAIN, self.record_name, self.record_content, self.record_ttl 148 | ) 149 | 150 | def test_del_txt_record_fail_to_find_domain(self): 151 | self._register_response("login", response="FAKE_SESSION") 152 | self._register_response("dns_zone_get_id", message="Not Found") 153 | with self.assertRaises(errors.PluginError): 154 | self.client.del_txt_record( 155 | DOMAIN, self.record_name, self.record_content, self.record_ttl 156 | ) 157 | 158 | def test_del_txt_record_fail_to_authenticate(self): 159 | self._register_response("login", message="FAILED") 160 | with self.assertRaises(errors.PluginError): 161 | self.client.del_txt_record( 162 | DOMAIN, self.record_name, self.record_content, self.record_ttl 163 | ) 164 | 165 | 166 | if __name__ == "__main__": 167 | unittest.main() # pragma: no cover 168 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "acme" 5 | version = "2.11.0" 6 | description = "ACME protocol implementation in Python" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "acme-2.11.0-py3-none-any.whl", hash = "sha256:23213ac3074a78862b219e0a30e141fd53238a8bdcf0668bd4dea59b28873fb8"}, 11 | {file = "acme-2.11.0.tar.gz", hash = "sha256:f4950015cf52ff0de12f37fc28034c7710aca63f64f1696253d2f6cb9f22645e"}, 12 | ] 13 | 14 | [package.dependencies] 15 | cryptography = ">=3.2.1" 16 | josepy = ">=1.13.0" 17 | PyOpenSSL = ">=17.5.0,<23.1.0 || >23.1.0" 18 | pyrfc3339 = "*" 19 | pytz = ">=2019.3" 20 | requests = ">=2.20.0" 21 | setuptools = ">=41.6.0" 22 | 23 | [package.extras] 24 | docs = ["Sphinx (>=1.0)", "sphinx-rtd-theme"] 25 | test = ["importlib-resources (>=1.3.1)", "pytest", "pytest-xdist", "typing-extensions"] 26 | 27 | [[package]] 28 | name = "certbot" 29 | version = "2.11.0" 30 | description = "ACME client" 31 | optional = false 32 | python-versions = ">=3.8" 33 | files = [ 34 | {file = "certbot-2.11.0-py3-none-any.whl", hash = "sha256:dc4e0a48bcb09448d60362170ca1047cc9a81966da0dd35135f2561f0ea7d5b1"}, 35 | {file = "certbot-2.11.0.tar.gz", hash = "sha256:257ae1cb0a534373ca50dd807c9ae96f27660e41379c45afb9b50cab0e6a7a97"}, 36 | ] 37 | 38 | [package.dependencies] 39 | acme = ">=2.11.0" 40 | ConfigArgParse = ">=1.5.3" 41 | configobj = ">=5.0.6" 42 | cryptography = ">=3.2.1" 43 | distro = ">=1.0.1" 44 | importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} 45 | importlib-resources = {version = ">=1.3.1", markers = "python_version < \"3.9\""} 46 | josepy = ">=1.13.0" 47 | parsedatetime = ">=2.4" 48 | pyrfc3339 = "*" 49 | pytz = ">=2019.3" 50 | pywin32 = {version = ">=300", markers = "sys_platform == \"win32\""} 51 | setuptools = ">=41.6.0" 52 | 53 | [package.extras] 54 | all = ["Sphinx (>=1.2)", "azure-devops", "coverage", "ipdb", "mypy", "pip", "poetry (>=1.2.0)", "poetry-plugin-export (>=1.1.0)", "pylint", "pytest", "pytest-cov", "pytest-xdist", "setuptools", "sphinx-rtd-theme", "tox", "twine", "types-httplib2", "types-pyOpenSSL", "types-pyRFC3339", "types-pytz", "types-pywin32", "types-requests", "types-setuptools", "types-six", "wheel"] 55 | dev = ["azure-devops", "ipdb", "poetry (>=1.2.0)", "poetry-plugin-export (>=1.1.0)", "twine"] 56 | docs = ["Sphinx (>=1.2)", "sphinx-rtd-theme"] 57 | test = ["coverage", "mypy", "pip", "pylint", "pytest", "pytest-cov", "pytest-xdist", "setuptools", "tox", "types-httplib2", "types-pyOpenSSL", "types-pyRFC3339", "types-pytz", "types-pywin32", "types-requests", "types-setuptools", "types-six", "wheel"] 58 | 59 | [[package]] 60 | name = "certifi" 61 | version = "2024.7.4" 62 | description = "Python package for providing Mozilla's CA Bundle." 63 | optional = false 64 | python-versions = ">=3.6" 65 | files = [ 66 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 67 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 68 | ] 69 | 70 | [[package]] 71 | name = "cffi" 72 | version = "1.17.0" 73 | description = "Foreign Function Interface for Python calling C code." 74 | optional = false 75 | python-versions = ">=3.8" 76 | files = [ 77 | {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, 78 | {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, 79 | {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, 80 | {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, 81 | {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, 82 | {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, 83 | {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, 84 | {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, 85 | {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, 86 | {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, 87 | {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, 88 | {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, 89 | {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, 90 | {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, 91 | {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, 92 | {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, 93 | {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, 94 | {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, 95 | {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, 96 | {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, 97 | {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, 98 | {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, 99 | {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, 100 | {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, 101 | {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, 102 | {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, 103 | {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, 104 | {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, 105 | {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, 106 | {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, 107 | {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, 108 | {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, 109 | {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, 110 | {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, 111 | {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, 112 | {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, 113 | {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, 114 | {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, 115 | {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, 116 | {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, 117 | {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, 118 | {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, 119 | {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, 120 | {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, 121 | {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, 122 | {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, 123 | {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, 124 | {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, 125 | {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, 126 | {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, 127 | {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, 128 | {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, 129 | {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, 130 | {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, 131 | {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, 132 | {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, 133 | {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, 134 | {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, 135 | {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, 136 | {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, 137 | {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, 138 | {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, 139 | {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, 140 | {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, 141 | {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, 142 | {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, 143 | {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, 144 | ] 145 | 146 | [package.dependencies] 147 | pycparser = "*" 148 | 149 | [[package]] 150 | name = "charset-normalizer" 151 | version = "3.3.2" 152 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 153 | optional = false 154 | python-versions = ">=3.7.0" 155 | files = [ 156 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 157 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 158 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 159 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 160 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 161 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 162 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 163 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 164 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 165 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 166 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 167 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 168 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 169 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 170 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 171 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 172 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 173 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 174 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 175 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 176 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 177 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 178 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 179 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 180 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 181 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 182 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 183 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 184 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 185 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 186 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 187 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 188 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 189 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 190 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 191 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 192 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 193 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 194 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 195 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 196 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 197 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 198 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 199 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 200 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 201 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 202 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 203 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 204 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 205 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 206 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 207 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 208 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 209 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 210 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 211 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 212 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 213 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 214 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 215 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 216 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 217 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 218 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 219 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 220 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 221 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 222 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 223 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 224 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 225 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 226 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 227 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 228 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 229 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 230 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 231 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 232 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 233 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 234 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 235 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 236 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 237 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 238 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 239 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 240 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 241 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 242 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 243 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 244 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 245 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 246 | ] 247 | 248 | [[package]] 249 | name = "configargparse" 250 | version = "1.7" 251 | description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." 252 | optional = false 253 | python-versions = ">=3.5" 254 | files = [ 255 | {file = "ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b"}, 256 | {file = "ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1"}, 257 | ] 258 | 259 | [package.extras] 260 | test = ["PyYAML", "mock", "pytest"] 261 | yaml = ["PyYAML"] 262 | 263 | [[package]] 264 | name = "configobj" 265 | version = "5.0.8" 266 | description = "Config file reading, writing and validation." 267 | optional = false 268 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 269 | files = [ 270 | {file = "configobj-5.0.8-py2.py3-none-any.whl", hash = "sha256:a7a8c6ab7daade85c3f329931a807c8aee750a2494363934f8ea84d8a54c87ea"}, 271 | {file = "configobj-5.0.8.tar.gz", hash = "sha256:6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97"}, 272 | ] 273 | 274 | [package.dependencies] 275 | six = "*" 276 | 277 | [[package]] 278 | name = "cryptography" 279 | version = "43.0.0" 280 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 281 | optional = false 282 | python-versions = ">=3.7" 283 | files = [ 284 | {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, 285 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, 286 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, 287 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, 288 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, 289 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, 290 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, 291 | {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, 292 | {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, 293 | {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, 294 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, 295 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, 296 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, 297 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, 298 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, 299 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, 300 | {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, 301 | {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, 302 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, 303 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, 304 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, 305 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, 306 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, 307 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, 308 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, 309 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, 310 | {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, 311 | ] 312 | 313 | [package.dependencies] 314 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 315 | 316 | [package.extras] 317 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 318 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 319 | nox = ["nox"] 320 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 321 | sdist = ["build"] 322 | ssh = ["bcrypt (>=3.1.5)"] 323 | test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 324 | test-randomorder = ["pytest-randomly"] 325 | 326 | [[package]] 327 | name = "distro" 328 | version = "1.9.0" 329 | description = "Distro - an OS platform information API" 330 | optional = false 331 | python-versions = ">=3.6" 332 | files = [ 333 | {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, 334 | {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, 335 | ] 336 | 337 | [[package]] 338 | name = "idna" 339 | version = "3.8" 340 | description = "Internationalized Domain Names in Applications (IDNA)" 341 | optional = false 342 | python-versions = ">=3.6" 343 | files = [ 344 | {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, 345 | {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, 346 | ] 347 | 348 | [[package]] 349 | name = "importlib-metadata" 350 | version = "8.4.0" 351 | description = "Read metadata from Python packages" 352 | optional = false 353 | python-versions = ">=3.8" 354 | files = [ 355 | {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, 356 | {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, 357 | ] 358 | 359 | [package.dependencies] 360 | zipp = ">=0.5" 361 | 362 | [package.extras] 363 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 364 | perf = ["ipython"] 365 | test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] 366 | 367 | [[package]] 368 | name = "importlib-resources" 369 | version = "6.4.4" 370 | description = "Read resources from Python packages" 371 | optional = false 372 | python-versions = ">=3.8" 373 | files = [ 374 | {file = "importlib_resources-6.4.4-py3-none-any.whl", hash = "sha256:dda242603d1c9cd836c3368b1174ed74cb4049ecd209e7a1a0104620c18c5c11"}, 375 | {file = "importlib_resources-6.4.4.tar.gz", hash = "sha256:20600c8b7361938dc0bb2d5ec0297802e575df486f5a544fa414da65e13721f7"}, 376 | ] 377 | 378 | [package.dependencies] 379 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 380 | 381 | [package.extras] 382 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 383 | cover = ["pytest-cov"] 384 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 385 | enabler = ["pytest-enabler (>=2.2)"] 386 | test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] 387 | type = ["pytest-mypy"] 388 | 389 | [[package]] 390 | name = "josepy" 391 | version = "1.13.0" 392 | description = "JOSE protocol implementation in Python" 393 | optional = false 394 | python-versions = ">=3.6" 395 | files = [ 396 | {file = "josepy-1.13.0-py2.py3-none-any.whl", hash = "sha256:6f64eb35186aaa1776b7a1768651b1c616cab7f9685f9660bffc6491074a5390"}, 397 | {file = "josepy-1.13.0.tar.gz", hash = "sha256:8931daf38f8a4c85274a0e8b7cb25addfd8d1f28f9fb8fbed053dd51aec75dc9"}, 398 | ] 399 | 400 | [package.dependencies] 401 | cryptography = ">=1.5" 402 | PyOpenSSL = ">=0.13" 403 | setuptools = ">=1.0" 404 | 405 | [package.extras] 406 | dev = ["pytest", "tox"] 407 | docs = ["Sphinx (>=1.0)", "sphinx-rtd-theme (>=1.0)"] 408 | tests = ["coverage (>=4.0)", "flake8 (<4)", "isort", "mypy", "pytest (>=2.8.0)", "pytest-cov", "pytest-flake8 (>=0.5)", "types-pyOpenSSL", "types-pyRFC3339", "types-requests", "types-setuptools"] 409 | 410 | [[package]] 411 | name = "mock" 412 | version = "5.1.0" 413 | description = "Rolling backport of unittest.mock for all Pythons" 414 | optional = false 415 | python-versions = ">=3.6" 416 | files = [ 417 | {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, 418 | {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, 419 | ] 420 | 421 | [package.extras] 422 | build = ["blurb", "twine", "wheel"] 423 | docs = ["sphinx"] 424 | test = ["pytest", "pytest-cov"] 425 | 426 | [[package]] 427 | name = "parsedatetime" 428 | version = "2.6" 429 | description = "Parse human-readable date/time text." 430 | optional = false 431 | python-versions = "*" 432 | files = [ 433 | {file = "parsedatetime-2.6-py3-none-any.whl", hash = "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"}, 434 | {file = "parsedatetime-2.6.tar.gz", hash = "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455"}, 435 | ] 436 | 437 | [[package]] 438 | name = "pycparser" 439 | version = "2.22" 440 | description = "C parser in Python" 441 | optional = false 442 | python-versions = ">=3.8" 443 | files = [ 444 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 445 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 446 | ] 447 | 448 | [[package]] 449 | name = "pyopenssl" 450 | version = "24.2.1" 451 | description = "Python wrapper module around the OpenSSL library" 452 | optional = false 453 | python-versions = ">=3.7" 454 | files = [ 455 | {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, 456 | {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, 457 | ] 458 | 459 | [package.dependencies] 460 | cryptography = ">=41.0.5,<44" 461 | 462 | [package.extras] 463 | docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] 464 | test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] 465 | 466 | [[package]] 467 | name = "pyrfc3339" 468 | version = "1.1" 469 | description = "Generate and parse RFC 3339 timestamps" 470 | optional = false 471 | python-versions = "*" 472 | files = [ 473 | {file = "pyRFC3339-1.1-py2.py3-none-any.whl", hash = "sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4"}, 474 | {file = "pyRFC3339-1.1.tar.gz", hash = "sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a"}, 475 | ] 476 | 477 | [package.dependencies] 478 | pytz = "*" 479 | 480 | [[package]] 481 | name = "pytz" 482 | version = "2024.1" 483 | description = "World timezone definitions, modern and historical" 484 | optional = false 485 | python-versions = "*" 486 | files = [ 487 | {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, 488 | {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, 489 | ] 490 | 491 | [[package]] 492 | name = "pywin32" 493 | version = "306" 494 | description = "Python for Window Extensions" 495 | optional = false 496 | python-versions = "*" 497 | files = [ 498 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 499 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 500 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 501 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 502 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 503 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 504 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 505 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 506 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 507 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 508 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 509 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 510 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 511 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 512 | ] 513 | 514 | [[package]] 515 | name = "requests" 516 | version = "2.32.3" 517 | description = "Python HTTP for Humans." 518 | optional = false 519 | python-versions = ">=3.8" 520 | files = [ 521 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 522 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 523 | ] 524 | 525 | [package.dependencies] 526 | certifi = ">=2017.4.17" 527 | charset-normalizer = ">=2,<4" 528 | idna = ">=2.5,<4" 529 | urllib3 = ">=1.21.1,<3" 530 | 531 | [package.extras] 532 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 533 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 534 | 535 | [[package]] 536 | name = "requests-mock" 537 | version = "1.12.1" 538 | description = "Mock out responses from the requests package" 539 | optional = false 540 | python-versions = ">=3.5" 541 | files = [ 542 | {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, 543 | {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, 544 | ] 545 | 546 | [package.dependencies] 547 | requests = ">=2.22,<3" 548 | 549 | [package.extras] 550 | fixture = ["fixtures"] 551 | 552 | [[package]] 553 | name = "setuptools" 554 | version = "74.0.0" 555 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 556 | optional = false 557 | python-versions = ">=3.8" 558 | files = [ 559 | {file = "setuptools-74.0.0-py3-none-any.whl", hash = "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f"}, 560 | {file = "setuptools-74.0.0.tar.gz", hash = "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e"}, 561 | ] 562 | 563 | [package.extras] 564 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] 565 | core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] 566 | cover = ["pytest-cov"] 567 | 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)"] 568 | enabler = ["pytest-enabler (>=2.2)"] 569 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] 570 | type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] 571 | 572 | [[package]] 573 | name = "six" 574 | version = "1.16.0" 575 | description = "Python 2 and 3 compatibility utilities" 576 | optional = false 577 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 578 | files = [ 579 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 580 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 581 | ] 582 | 583 | [[package]] 584 | name = "urllib3" 585 | version = "2.2.2" 586 | description = "HTTP library with thread-safe connection pooling, file post, and more." 587 | optional = false 588 | python-versions = ">=3.8" 589 | files = [ 590 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 591 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 592 | ] 593 | 594 | [package.extras] 595 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 596 | h2 = ["h2 (>=4,<5)"] 597 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 598 | zstd = ["zstandard (>=0.18.0)"] 599 | 600 | [[package]] 601 | name = "zipp" 602 | version = "3.20.1" 603 | description = "Backport of pathlib-compatible object wrapper for zip files" 604 | optional = false 605 | python-versions = ">=3.8" 606 | files = [ 607 | {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, 608 | {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, 609 | ] 610 | 611 | [package.extras] 612 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 613 | cover = ["pytest-cov"] 614 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 615 | enabler = ["pytest-enabler (>=2.2)"] 616 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 617 | type = ["pytest-mypy"] 618 | 619 | [metadata] 620 | lock-version = "2.0" 621 | python-versions = ">=3.8" 622 | content-hash = "4027ac2467d51742589e74ab4c8e8d3d3083e6bd3fb33c4e40277ad635190b4b" 623 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [project] 3 | name = "certbot-dns-ispconfig" 4 | version = "0.3.0" 5 | description = "ispconfig DNS Authenticator plugin for Certbot" 6 | authors = [ 7 | { name = "Matthias Bilger", email = "matthias@bilger.info" } 8 | ] 9 | license = { text = "Apache-2.0" } 10 | readme = "README.rst" 11 | homepage = "https://github.com/m42e/certbot-dns-ispconfig" 12 | keywords = ["certbot", "dns", "ispconfig", "authenticator", "plugin"] 13 | dependencies = [ 14 | "python>=3.8", 15 | "acme>=2.11.0", 16 | "certbot>=2.11.0", 17 | "setuptools", 18 | "requests", 19 | "mock", 20 | "requests-mock" 21 | ] 22 | 23 | [tool.poetry] 24 | name = "certbot-dns-ispconfig" 25 | version = "0.3.0" 26 | description = "ispconfig DNS Authenticator plugin for Certbot" 27 | authors = ["Matthias Bilger ‹matthias@bilger.info>"] 28 | license = "Apache License 2.0" 29 | readme = "README, rst" 30 | homepage = "https://github.com/m42e/certbot-dns-ispconfig" 31 | keywords = ["certbot", "dns", "ispconfig", "authenticator", "plugin"] 32 | 33 | [tool.poetry.dependencies] 34 | python = ">=3.8" 35 | acme = ">=2.11.0" 36 | certbot = ">=2.11.0" 37 | setuptools = "*" 38 | requests = "*" 39 | mock = "*" 40 | requests-mock = "*" 41 | 42 | [build-system] 43 | requires = ["hatchling"] 44 | build-backend = "hatchling.build" 45 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup 3 | from setuptools import find_packages 4 | 5 | version = "0.3.0" 6 | 7 | install_requires = [ 8 | "acme>=0.29.0", 9 | "certbot>=0.34.0", 10 | "setuptools", 11 | "requests", 12 | "mock", 13 | "requests-mock", 14 | ] 15 | 16 | # read the contents of your README file 17 | 18 | this_directory = path.abspath(path.dirname(__file__)) 19 | with open(path.join(this_directory, "README.rst")) as f: 20 | long_description = f.read() 21 | 22 | setup( 23 | name="certbot-dns-ispconfig", 24 | version=version, 25 | description="ispconfig DNS Authenticator plugin for Certbot", 26 | long_description=long_description, 27 | long_description_content_type="text/x-rst", 28 | url="https://github.com/m42e/certbot-dns-ispconfig", 29 | author="Matthias Bilger", 30 | author_email="matthias@bilger.info", 31 | license="Apache License 2.0", 32 | python_requires=">=3.8", 33 | classifiers=[ 34 | "Development Status :: 3 - Alpha", 35 | "Environment :: Plugins", 36 | "Intended Audience :: System Administrators", 37 | "License :: OSI Approved :: Apache Software License", 38 | "Operating System :: POSIX :: Linux", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 3.8", 41 | "Programming Language :: Python :: 3.9", 42 | "Programming Language :: Python :: 3.10", 43 | "Programming Language :: Python :: 3.11", 44 | "Topic :: Internet :: WWW/HTTP", 45 | "Topic :: Security", 46 | "Topic :: System :: Installation/Setup", 47 | "Topic :: System :: Networking", 48 | "Topic :: System :: Systems Administration", 49 | "Topic :: Utilities", 50 | ], 51 | packages=find_packages(), 52 | include_package_data=True, 53 | install_requires=install_requires, 54 | entry_points={ 55 | "certbot.plugins": [ 56 | "dns-ispconfig = certbot_dns_ispconfig.dns_ispconfig:Authenticator" 57 | ] 58 | }, 59 | test_suite="certbot_dns_ispconfig", 60 | ) 61 | --------------------------------------------------------------------------------