From 4871fe4f0a594b7232b2f67f94af7c19076a6c84 Mon Sep 17 00:00:00 2001 From: Ramin Haeri Azad Date: Mon, 16 Jun 2025 17:22:33 -0400 Subject: [PATCH 1/4] Started using requests instead of pyURL --- .gitignore | 3 + obiba_agate/console.py | 1 + obiba_agate/core.py | 308 +++++++++++++++++++++++++++++------------ obiba_agate/rest.py | 63 +++++---- poetry.lock | 245 +++++++++++++++++++++++--------- pyproject.toml | 3 +- tests/test_restCall.py | 15 +- 7 files changed, 446 insertions(+), 192 deletions(-) diff --git a/.gitignore b/.gitignore index ffdc42e..ededdd0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ coverage *.sw? __pycache__ + +debug.py +.history \ No newline at end of file diff --git a/obiba_agate/console.py b/obiba_agate/console.py index f4f1324..15d294d 100755 --- a/obiba_agate/console.py +++ b/obiba_agate/console.py @@ -24,6 +24,7 @@ def add_agate_arguments(parser): parser.add_argument('--ssl-cert', '-sc', required=False, help='Certificate (public key) file') parser.add_argument('--ssl-key', '-sk', required=False, help='Private key file') parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + parser.add_argument('--no-ssl-verify', '-nv', action='store_true', help='Do not verify SSL certificates for HTTPS.') def add_subcommand(subparsers, name, help, add_args_func, default_func): diff --git a/obiba_agate/core.py b/obiba_agate/core.py index f8388ec..5574e5e 100755 --- a/obiba_agate/core.py +++ b/obiba_agate/core.py @@ -4,11 +4,15 @@ Curl options http://curl.haxx.se/libcurl/c/curl_easy_setopt.html """ -import pycurl import base64 import json import os.path import getpass +from requests import Session, Request +import urllib3 +from http.client import HTTPConnection +from http import HTTPStatus + import urllib.request, urllib.parse, urllib.error from functools import reduce @@ -19,21 +23,27 @@ class AgateClient: """ def __init__(self, server=None): - self.curl_options = {} + self.session = Session() self.headers = {} self.base_url = self.__ensure_entry('Agate address', server) + def __del__(self): + self.close() + @classmethod def build(cls, loginInfo): return AgateClient.buildWithAuthentication(loginInfo.data['server'], loginInfo.data['user'], - loginInfo.data['password'], loginInfo.data['otp']) + loginInfo.data['password'], loginInfo.data['otp'], + loginInfo.data["no_ssl_verify"]) @classmethod - def buildWithAuthentication(cls, server, user, password, otp=None): + def buildWithAuthentication(cls, server, user, password, otp=None, no_ssl_verify: bool = False): client = cls(server) if client.base_url.startswith('https:'): - client.verify_peer(0) - client.verify_host(0) + client.session.verify = not no_ssl_verify + if no_ssl_verify: + urllib3.disable_warnings() + client.credentials(user, password, otp) return client @@ -52,7 +62,8 @@ def credentials(self, user, password, otp): if otp: val = input("Enter 6-digits code: ") self.header('X-Obiba-TOTP', val) - return self.header('Authorization', 'Basic ' + base64.b64encode((u + ':' + p).encode("utf-8")).decode("utf-8")) + + self.session.headers.update({"Authorization": "Basic %s" % base64.b64encode(("%s:%s" % (u, p)).encode("utf-8")).decode("utf-8")}) def keys(self, cert_file, key_file, key_pwd=None, ca_certs=None): self.curl_option(pycurl.SSLCERT, cert_file) @@ -64,26 +75,41 @@ def keys(self, cert_file, key_file, key_pwd=None, ca_certs=None): self.headers.pop('Authorization', None) return self - def verify_peer(self, verify): - return self.curl_option(pycurl.SSL_VERIFYPEER, verify) - - def verify_host(self, verify): - return self.curl_option(pycurl.SSL_VERIFYHOST, verify) - - def ssl_version(self, version): - return self.curl_option(pycurl.SSLVERSION, version) + def verify(self, value): + """ + Ignore or validate certificate - def curl_option(self, opt, value): - self.curl_options[opt] = value + :param value = True/False to validation or not. Value can also be a CA_BUNDLE file or directory (e.g. 'verify=/etc/ssl/certs/ca-certificates.crt') + """ + self.session.verify = value return self def header(self, key, value): - self.headers[key] = value + """ + Adds a header to session headers used by the request + + :param key - header key + :param value - header value + """ + header = {} + header[key] = value + + self.session.headers.update(header) return self def new_request(self): return AgateRequest(self) + def close(self): + """ + Closes client session and request to close Agate server session + """ + try: + self.new_request().resource("/auth/session/_current").delete().send() + self.session.close() + except Exception as e: + pass + class LoginInfo: data = None @@ -92,18 +118,17 @@ def parse(cls, args): data = {} argv = vars(args) + data["no_ssl_verify"] = argv.get("no_ssl_verify") + if argv.get('agate'): data['server'] = argv['agate'] else: raise Exception('Agate server information is missing.') if argv.get('user') and argv.get('password'): - data['user'] = argv['user'] - data['password'] = argv['password'] - data['otp'] = argv['otp'] - elif argv.get('ssl_cert') and argv.get('ssl_key'): - data['cert'] = argv['ssl_cert'] - data['key'] = argv['ssl_key'] + data["user"] = argv["user"] + data["password"] = argv["password"] + data["otp"] = argv["otp"] else: raise Exception('Invalid login information. Requires user-password or certificate-key information') @@ -123,37 +148,55 @@ class AgateRequest: def __init__(self, agate_client): self.client = agate_client - self.curl_options = {} + self.options = {} self.headers = {'Accept': 'application/json'} - self._verbose = False + self.params = {} + self._fail_on_error = False + self.files = None + self.data = None - def curl_option(self, opt, value): - self.curl_options[opt] = value - return self def timeout(self, value): - return self.curl_option(pycurl.TIMEOUT, value) + """ + Sets the connection and read timeout + Note: value can be a tupple to have different timeouts for connection and reading (connTimout, readTimeout) - def connection_timeout(self, value): - return self.curl_option(pycurl.CONNECTTIMEOUT, value) + :param value - connection/read timout + """ + self.options["timeout"] = value + return self def verbose(self): + """ + Enables the verbose mode + """ + HTTPConnection.debuglevel = 1 self._verbose = True - return self.curl_option(pycurl.VERBOSE, True) + return self def fail_on_error(self): - return self.curl_option(pycurl.FAILONERROR, True) + self._fail_on_error = True + return self def header(self, key, value): + """ + Adds a header to session headers used by the request + + :param key - header key + :param value - header value + """ if value: - self.headers[key] = value + header = {} + header[key] = value + self.headers.update(header) return self def accept(self, value): - return self.header('Accept', value) + self.headers.update({"Accept": value}) + return self def content_type(self, value): - return self.header('Content-Type', value) + return self.accept("application/json") def accept_json(self): return self.accept('application/json') @@ -162,12 +205,17 @@ def content_type_json(self): return self.content_type('application/json') def method(self, method): + """ + Sets a HTTP method + + :param method - any of ['GET', 'DELETE', 'PUT', 'POST', 'OPTIONS'] + """ if not method: - self.method = 'GET' - elif method in ['GET', 'DELETE', 'PUT', 'POST', 'OPTIONS']: + self.method = "GET" + elif method in ["GET", "DELETE", "PUT", "POST", "OPTIONS"]: self.method = method else: - raise Exception('Not a valid method: ' + method) + raise ValueError("Not a valid method: " + method) return self def get(self): @@ -186,68 +234,86 @@ def options(self): return self.method('OPTIONS') def __build_request(self): - curl = pycurl.Curl() - # curl options - for o in self.client.curl_options: - curl.setopt(o, self.client.curl_options[o]) - for o in self.curl_options: - curl.setopt(o, self.curl_options[o]) - # headers - hlist = [] - for h in self.client.headers: - hlist.append(h + ": " + self.client.headers[h]) - for h in self.headers: - hlist.append(h + ": " + self.headers[h]) - curl.setopt(pycurl.HTTPHEADER, hlist) - if self.method: - curl.setopt(pycurl.CUSTOMREQUEST, self.method) + """ + Builder method creating a Request object to be sent by the client session object + """ + request = Request() + request.method = self.method if self.method else "GET" + + for option in self.options: + setattr(request, option, self.options[option]) + + # Combine the client and the request headers + request.headers = {} + request.headers.update(self.client.session.headers) + request.headers.update(self.headers) + if self.resource: - curl.setopt(pycurl.URL, self.client.base_url + '/ws' + self.resource) + path = self.resource + request.url = self.client.base_url + "/ws" + path + + if self.params: + request.params = self.params else: - raise Exception('Resource is missing') - return curl + raise ValueError("Resource is missing") + + if self.files is not None: + request.files = self.files + + if self.data is not None: + request.data = self.data + + return request + def resource(self, ws): self.resource = ws return self + def form(self, parameters): + """ + Stores the request's body as a form + Note: no need to transform parameters in key=value pairs + + :param parametes - parameters as a dict value + """ + return self.content(parameters) + def content(self, content): + """ + Stores the request body + + :param content - request body + """ if self._verbose: - print('* Content:') + print("* Content:") print(content) - encodedContent = content.encode('utf-8') - self.curl_option(pycurl.POST, 1) - self.curl_option(pycurl.POSTFIELDSIZE, len(encodedContent)) - self.curl_option(pycurl.POSTFIELDS, encodedContent) - return self - def content_file(self, filename): - if self._verbose: - print('* File Content:') - print('[file=' + filename + ', size=' + str(os.path.getsize(filename)) + ']') - self.curl_option(pycurl.POST, 1) - self.curl_option(pycurl.POSTFIELDSIZE, os.path.getsize(filename)) - reader = open(filename, 'rb') - self.curl_option(pycurl.READFUNCTION, reader.read) + self.data = content return self + def content_upload(self, filename): + """ + Sets the file associate with the upload + + Note: Requests library takes care of mutlti-part setting in the header + """ if self._verbose: - print('* File Content:') - print('[file=' + filename + ', size=' + str(os.path.getsize(filename)) + ']') - # self.curl_option(pycurl.POST,1) - self.curl_option(pycurl.HTTPPOST, [("file1", (pycurl.FORM_FILE, filename))]) + print("* File Content:") + print("[file=" + filename + ", size=" + str(os.path.getsize(filename)) + "]") + self.files = {"file": (filename, open(filename, "rb"))} return self def send(self): - curl = self.__build_request() - hbuf = HeaderStorage() - cbuf = Storage() - curl.setopt(curl.WRITEFUNCTION, cbuf.store) - curl.setopt(curl.HEADERFUNCTION, hbuf.store) - curl.perform() - response = AgateResponse(curl.getinfo(pycurl.HTTP_CODE), hbuf.headers, cbuf.content) - curl.close() + """ + Sends the request via client session object + """ + request = self.__build_request() + response = AgateResponse(self.client.session.send(request.prepare())) + + if self._fail_on_error and response.code >= 400: + raise HTTPError(response) return response @@ -298,20 +364,56 @@ class AgateResponse: Response from Agate: code, headers and content """ - def __init__(self, code, headers, content): - self.code = code - self.headers = headers - self.content = content + def __init__(self, response): + self.response = response + + @property + def code(self): + return self.response.status_code + + @property + def headers(self): + return self.response.headers + + @property + def content(self): + return self.response.content + + @property + def version(self): + return self.headers.get("X-Agate-Version", None) + + @property + def version_info(self): + agateVersion = self.version + if agateVersion is not None: + info = {} + [info["major"], info["minor"], info["patch"]] = self.version.split(".") + return info + return None def as_json(self): - return json.loads(self.content) + """ + Returns response body as a JSON document + """ + if self.response is None or self.response.content is None: + return None + + try: + return self.response.json() + except Exception as e: + if type(self.response.content) == str: + return self.response.content + else: + # FIXME silently fail + return None def pretty_json(self): + """ + Beatifies the JSON response + """ return json.dumps(self.as_json(), sort_keys=True, indent=2) - def __str__(self): - return self.content - class UriBuilder: """ @@ -357,3 +459,25 @@ def concat_query(q, p): def build(self): return self.__str__() + +class HTTPError(Exception): + """ + HTTP related error class + """ + + def __init__(self, response: AgateResponse, message: str = None): + # Call the base class constructor with the parameters it needs + super().__init__(message if message else "HTTP Error: %s" % response.code) + self.code = response.code + http_status = [x for x in list(HTTPStatus) if x.value == response.code][0] + self.message = message if message else "%s: %s" % (http_status.phrase, http_status.description) + self.error = response.as_json() if response.content else {"code": response.code, "status": self.message} + # case the reported error is not a dict + if type(self.error) != dict: + self.error = {"code": response.code, "status": self.error} + + def is_client_error(self) -> bool: + return self.code >= 400 and self.code < 500 + + def is_server_error(self) -> bool: + return self.code >= 500 \ No newline at end of file diff --git a/obiba_agate/rest.py b/obiba_agate/rest.py index eb9e2e3..a2a45ef 100755 --- a/obiba_agate/rest.py +++ b/obiba_agate/rest.py @@ -24,32 +24,37 @@ def do_command(args): Execute REST command """ # Build and send request - request = core.AgateClient.build(core.AgateClient.LoginInfo.parse(args)).new_request() - request.fail_on_error() - - if args.accept: - request.accept(args.accept) - else: - request.accept_json() - - if args.content_type: - request.content_type(args.content_type) - print('Enter content:') - request.content(sys.stdin.read()) - - if args.verbose: - request.verbose() - - # send request - request.method(args.method).resource(args.ws) - response = request.send() - - # format response - res = response.content - if args.json: - res = response.pretty_json() - elif args.method in ['OPTIONS']: - res = response.headers['Allow'] - - # output to stdout - print(res) + client = core.AgateClient.build(core.AgateClient.LoginInfo.parse(args)) + + try: + request = client.new_request() + request.fail_on_error() + + if args.accept: + request.accept(args.accept) + else: + request.accept_json() + + if args.content_type: + request.content_type(args.content_type) + print('Enter content:') + request.content(sys.stdin.read()) + + if args.verbose: + request.verbose() + + # send request + request.method(args.method).resource(args.ws) + response = request.send() + + # format response + res = response.content + if args.json: + res = response.pretty_json() + elif args.method in ['OPTIONS']: + res = response.headers['Allow'] + + # output to stdout + print(res) + finally: + client.close() \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 77308b9..bd536f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,13 +1,126 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2025.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["test"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -17,10 +130,9 @@ files = [ name = "exceptiongroup" version = "1.3.0" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -32,14 +144,28 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" version = "6.7.0" description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version == \"3.7\"" files = [ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, @@ -52,15 +178,15 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -70,9 +196,9 @@ files = [ name = "packaging" version = "24.0" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, @@ -82,9 +208,9 @@ files = [ name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, @@ -97,54 +223,13 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycurl" -version = "7.45.6" -description = "PycURL -- A Python Interface To The cURL library" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "pycurl-7.45.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c31b390f1e2cd4525828f1bb78c1f825c0aab5d1588228ed71b22c4784bdb593"}, - {file = "pycurl-7.45.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:942b352b69184cb26920db48e0c5cb95af39874b57dbe27318e60f1e68564e37"}, - {file = "pycurl-7.45.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3441ee77e830267aa6e2bb43b29fd5f8a6bd6122010c76a6f0bf84462e9ea9c7"}, - {file = "pycurl-7.45.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2a21e13278d7553a04b421676c458449f6c10509bebf04993f35154b06ee2b20"}, - {file = "pycurl-7.45.6-cp310-cp310-win32.whl", hash = "sha256:d0b5501d527901369aba307354530050f56cd102410f2a3bacd192dc12c645e3"}, - {file = "pycurl-7.45.6-cp310-cp310-win_amd64.whl", hash = "sha256:abe1b204a2f96f2eebeaf93411f03505b46d151ef6d9d89326e6dece7b3a008a"}, - {file = "pycurl-7.45.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f57ad26d6ab390391ad5030790e3f1a831c1ee54ad3bf969eb378f5957eeb0a"}, - {file = "pycurl-7.45.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6fd295f03c928da33a00f56c91765195155d2ac6f12878f6e467830b5dce5f5"}, - {file = "pycurl-7.45.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:334721ce1ccd71ff8e405470768b3d221b4393570ccc493fcbdbef4cd62e91ed"}, - {file = "pycurl-7.45.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0cd6b7794268c17f3c660162ed6381769ce0ad260331ef49191418dfc3a2d61a"}, - {file = "pycurl-7.45.6-cp311-cp311-win32.whl", hash = "sha256:357ea634395310085b9d5116226ac5ec218a6ceebf367c2451ebc8d63a6e9939"}, - {file = "pycurl-7.45.6-cp311-cp311-win_amd64.whl", hash = "sha256:878ae64484db18f8f10ba99bffc83fefb4fe8f5686448754f93ec32fa4e4ee93"}, - {file = "pycurl-7.45.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c872d4074360964697c39c1544fe8c91bfecbff27c1cdda1fee5498e5fdadcda"}, - {file = "pycurl-7.45.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56d1197eadd5774582b259cde4364357da71542758d8e917f91cc6ed7ed5b262"}, - {file = "pycurl-7.45.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8a99e56d2575aa74c48c0cd08852a65d5fc952798f76a34236256d5589bf5aa0"}, - {file = "pycurl-7.45.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c04230b9e9cfdca9cf3eb09a0bec6cf2f084640f1f1ca1929cca51411af85de2"}, - {file = "pycurl-7.45.6-cp312-cp312-win32.whl", hash = "sha256:ae893144b82d72d95c932ebdeb81fc7e9fde758e5ecd5dd10ad5b67f34a8b8ee"}, - {file = "pycurl-7.45.6-cp312-cp312-win_amd64.whl", hash = "sha256:56f841b6f2f7a8b2d3051b9ceebd478599dbea3c8d1de8fb9333c895d0c1eea5"}, - {file = "pycurl-7.45.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c09b7180799af70fc1d4eed580cfb1b9f34fda9081f73a3e3bc9a0e5a4c0e9b"}, - {file = "pycurl-7.45.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:361bf94b2a057c7290f9ab84e935793ca515121fc012f4b6bef6c3b5e4ea4397"}, - {file = "pycurl-7.45.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb9eff0c7794af972da769a887c87729f1bcd8869297b1c01a2732febbb75876"}, - {file = "pycurl-7.45.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26839d43dc7fff6b80e0067f185cc1d0e9be2ae6e2e2361ae8488cead5901c04"}, - {file = "pycurl-7.45.6-cp313-cp313-win32.whl", hash = "sha256:a721c2696a71b1aa5ecf82e6d0ade64bc7211b7317f1c9c66e82f82e2264d8b4"}, - {file = "pycurl-7.45.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0198ebcda8686b3a0c66d490a687fa5fd466f8ecc2f20a0ed0931579538ae3d"}, - {file = "pycurl-7.45.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a554a2813d415a7bb9a996a6298f3829f57e987635dcab9f1197b2dccd0ab3b2"}, - {file = "pycurl-7.45.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f721e3394e5bd7079802ec1819b19c5be4842012268cc45afcb3884efb31cf0"}, - {file = "pycurl-7.45.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:81005c0f681d31d5af694d1d3c18bbf1bed0bc8b2bb10fb7388cb1378ba9bd6a"}, - {file = "pycurl-7.45.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:3fc0b505c37c7c54d88ced27e1d9e3241130987c24bf1611d9bbd9a3e499e07c"}, - {file = "pycurl-7.45.6-cp39-cp39-win32.whl", hash = "sha256:1309fc0f558a80ca444a3a5b0bdb1572a4d72b195233f0e65413b4d4dd78809b"}, - {file = "pycurl-7.45.6-cp39-cp39-win_amd64.whl", hash = "sha256:2d1a49418b8b4c61f52e06d97b9c16142b425077bd997a123a2ba9ef82553203"}, - {file = "pycurl-7.45.6.tar.gz", hash = "sha256:2b73e66b22719ea48ac08a93fc88e57ef36d46d03cb09d972063c9aa86bb74e6"}, -] - [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -162,14 +247,35 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -179,23 +285,38 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" files = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] +[[package]] +name = "urllib3" +version = "1.26.15" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version == \"3.7\"" files = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, @@ -203,9 +324,9 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.7" -content-hash = "62f3bc5253b6427b5e6c14a397459c9045060a937dd39e1f553c1b2446698f52" +content-hash = "1ed0e72a1b6f9dc1b844998404d35939579ca909d881179bc4f6e22bc072371c" diff --git a/pyproject.toml b/pyproject.toml index 5e5ee24..9a9aa44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ packages = [{include = "obiba_agate"}] [tool.poetry.dependencies] python = "^3.7" -pycurl = "^7.45.2" +requests = "^2.31.0" +urllib3 = "1.26.15" [tool.poetry.group.test.dependencies] pytest = "^7.2.2" diff --git a/tests/test_restCall.py b/tests/test_restCall.py index 14c59ed..6342476 100644 --- a/tests/test_restCall.py +++ b/tests/test_restCall.py @@ -33,14 +33,13 @@ def test_sendRest(self): self.fail(e) def test_sendSecuredRest(self): - if exists(self.SSL_CERTIFICATE): - try: - client = AgateClient.buildWithCertificate(server=self.SERVER, - cert=self.SSL_CERTIFICATE, - key=self.SSL_KEY) - self.__sendSimpleRequest(client.new_request()) - except Exception as e: - self.fail(e) + try: + client = AgateClient.buildWithAuthentication(server='https://localhost:8444', + user='administrator', + password='password', no_ssl_verify=True) + self.__sendSimpleRequest(client.new_request()) + except Exception as e: + self.fail(e) def test_validAuthLoginInfo(self): try: From 40c8314630298f5fe74fb76ce2943947a82709d1 Mon Sep 17 00:00:00 2001 From: Ramin Haeri Azad Date: Tue, 17 Jun 2025 09:37:52 -0400 Subject: [PATCH 2/4] Removed any references to pycurl --- obiba_agate/core.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/obiba_agate/core.py b/obiba_agate/core.py index 5574e5e..eaa820b 100755 --- a/obiba_agate/core.py +++ b/obiba_agate/core.py @@ -1,7 +1,5 @@ """ -Based on PyCurl http://pycurl.sourceforge.net/ -See also http://www.angryobjects.com/2011/10/15/http-with-python-pycurl-by-example/ -Curl options http://curl.haxx.se/libcurl/c/curl_easy_setopt.html +Based on Python Request library https://docs.python-requests.org/en/latest/index.html """ import base64 @@ -12,8 +10,7 @@ import urllib3 from http.client import HTTPConnection from http import HTTPStatus - -import urllib.request, urllib.parse, urllib.error +import urllib.parse from functools import reduce @@ -65,16 +62,6 @@ def credentials(self, user, password, otp): self.session.headers.update({"Authorization": "Basic %s" % base64.b64encode(("%s:%s" % (u, p)).encode("utf-8")).decode("utf-8")}) - def keys(self, cert_file, key_file, key_pwd=None, ca_certs=None): - self.curl_option(pycurl.SSLCERT, cert_file) - self.curl_option(pycurl.SSLKEY, key_file) - if key_pwd: - self.curl_option(pycurl.KEYPASSWD, key_pwd) - if ca_certs: - self.curl_option(pycurl.CAINFO, ca_certs) - self.headers.pop('Authorization', None) - return self - def verify(self, value): """ Ignore or validate certificate From 13233bb6f235925a4329ce749734428297c479d5 Mon Sep 17 00:00:00 2001 From: Ramin Haeri Azad Date: Tue, 17 Jun 2025 13:31:02 -0400 Subject: [PATCH 3/4] Completed using requests --- obiba_agate/core.py | 6 ++++-- tests/test_restCall.py | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/obiba_agate/core.py b/obiba_agate/core.py index eaa820b..b6d5149 100755 --- a/obiba_agate/core.py +++ b/obiba_agate/core.py @@ -137,6 +137,7 @@ def __init__(self, agate_client): self.client = agate_client self.options = {} self.headers = {'Accept': 'application/json'} + self._verbose = False self.params = {} self._fail_on_error = False self.files = None @@ -183,13 +184,14 @@ def accept(self, value): return self def content_type(self, value): - return self.accept("application/json") + return self.headers.update({"Content-Type": value}) def accept_json(self): return self.accept('application/json') def content_type_json(self): - return self.content_type('application/json') + self.content_type('application/json') + return self def method(self, method): """ diff --git a/tests/test_restCall.py b/tests/test_restCall.py index 6342476..0d0c456 100644 --- a/tests/test_restCall.py +++ b/tests/test_restCall.py @@ -27,16 +27,18 @@ def test_sendRestBadCredentials(self): def test_sendRest(self): try: client = AgateClient.buildWithAuthentication(server=self.SERVER, user='administrator', - password='password') + password='password', no_ssl_verify=True) self.__sendSimpleRequest(client.new_request()) except Exception as e: self.fail(e) def test_sendSecuredRest(self): try: - client = AgateClient.buildWithAuthentication(server='https://localhost:8444', - user='administrator', - password='password', no_ssl_verify=True) + client = AgateClient.buildWithAuthentication(server=self.SERVER, + user='administrator', + password='password') + # place a valid CRT to verify the SERVER SSL certificate + #client.verify('') self.__sendSimpleRequest(client.new_request()) except Exception as e: self.fail(e) From 21c114e6a06469bd252ebd502184bf3e337131e5 Mon Sep 17 00:00:00 2001 From: Ramin Haeri Azad Date: Tue, 17 Jun 2025 17:05:27 -0400 Subject: [PATCH 4/4] Corrected PR comments --- obiba_agate/core.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/obiba_agate/core.py b/obiba_agate/core.py index b6d5149..f46da28 100755 --- a/obiba_agate/core.py +++ b/obiba_agate/core.py @@ -184,7 +184,7 @@ def accept(self, value): return self def content_type(self, value): - return self.headers.update({"Content-Type": value}) + return self.header('Content-Type', value) def accept_json(self): return self.accept('application/json') @@ -291,7 +291,8 @@ def content_upload(self, filename): if self._verbose: print("* File Content:") print("[file=" + filename + ", size=" + str(os.path.getsize(filename)) + "]") - self.files = {"file": (filename, open(filename, "rb"))} + with open(filename, "rb") as file: + self.files = {"file": (filename, file.read())} return self def send(self): @@ -377,8 +378,13 @@ def version_info(self): agateVersion = self.version if agateVersion is not None: info = {} - [info["major"], info["minor"], info["patch"]] = self.version.split(".") - return info + version_parts = self.version.split(".") + if len(version_parts) == 3: + info["major"], info["minor"], info["patch"] = version_parts + return info + else: + # Handle malformed version string + return None return None def as_json(self):