From 0b182fc4a3add742aa69be43cb44901ac2093778 Mon Sep 17 00:00:00 2001 From: Frank Greguska <89428916+frankinspace@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:24:31 -0700 Subject: [PATCH 01/27] /version 4.1.0a0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea6b3ba..9dd4a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "3.2.0a3" +version = "4.1.0a0" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 158ec54a4620a5b8d0d62861cc5199a799840f16 Mon Sep 17 00:00:00 2001 From: frankinspace Date: Tue, 2 Jul 2024 16:19:01 +0000 Subject: [PATCH 02/27] /version 4.1.0a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9dd4a0b..192978e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a0" +version = "4.1.0a1" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 7c794eb7ec62fac6a141bc46d4d0f22e02bc8d34 Mon Sep 17 00:00:00 2001 From: Marjorie Lucas <47004511+marjo-luc@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:42:55 -0700 Subject: [PATCH 03/27] added query params to job list endpoint (#131) * added query params to job list endpoint * added parser args * updated hysds mozart util * added logging * test * test * fix * fix * debug * fix * cleanup * test * fix * review updates * added back line accidentally removed * added comment * Update api/utils/ogc_translate.py Co-authored-by: Sujen Shah * clean up --------- Co-authored-by: Sujen Shah --- api/endpoints/job.py | 74 +++++++++++++++++++++++++++++++------- api/utils/hysds_util.py | 51 ++++++++++++++++++++------ api/utils/ogc_translate.py | 28 +++++++++++++++ 3 files changed, 130 insertions(+), 23 deletions(-) diff --git a/api/endpoints/job.py b/api/endpoints/job.py index aeac9e3..ae9e835 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -455,29 +455,77 @@ def get(self, job_id): @ns.route('/job//list') class Jobs(Resource): parser = api.parser() - parser.add_argument('page_size', required=False, type=str, - help="Job Listing Pagination Size") - parser.add_argument('offset', required=False, type=str, - help="Job Listing Pagination Offset") + parser.add_argument('page_size', required=False, type=str, help="Job Listing Pagination Size") + parser.add_argument('offset', required=False, type=str, help="Job Listing Pagination Offset") + parser.add_argument('job_type', type=str, help="Job type + version, e.g. topsapp:v1.0", required=False) + parser.add_argument('tag', type=str, help="User-defined job tag", required=False) + parser.add_argument('queue', type=str, help="Submitted job queue", required=False) + parser.add_argument('priority', type=int, help="Job priority, 0-9", required=False) + parser.add_argument('start_time', type=str, help="Start time of @timestamp field", required=False) + parser.add_argument('end_time', type=str, help="Start time of @timestamp field", required=False) + parser.add_argument('get_job_details', type=bool, help="Return full details if True. List of job id's if false. Default True.", required=False) + parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", required=False) + @api.doc(security='ApiKeyAuth') @login_required def get(self, username): """ - This will return run a list of jobs for a specified user - :return: + Returns a list of jobs for a given user. + + :param username: Username + :param get_job_details: Boolean that returns job details if set to True or just job ID's if set to False. Default is True. + :param page_size: Page size for pagination + :param offset: Offset for pagination + :param status: Job status + :param end_time: End time + :param start_time: Start time + :param priority: Job priority + :param queue: Queue + :param tag: User tag + :param job_type: Algorithm type + :return: List of jobs for a given user that matches query params provided """ - offset = request.args.get("offset", 0) - page_size = request.args.get("page_size", 250) + + defaults = { + "username" : username, + "get_job_details" : True, # To preserve existing behavior, set default to True. In the future, we should set this to False. + "page_size" : 10, + "offset" : 0, + "status" : None, + "end_time" : None, + "start_time" : None, + "priority" : None, + "queue" : None, + "tag" : None, + "job_type" : None + } + + # Get params and set default values + params = {key: request.args.get(key, default) for key, default in defaults.items()} + + is_param_true = lambda x: x if isinstance(x, bool) else x.lower() == 'true' + get_job_details = is_param_true(params['get_job_details']) + + # If status is provided, make sure it is HySDS-compliant + if params['status'] is not None: + params['status'] = ogc.get_hysds_status_from_wps(params['status']) + + # Filter out the non-query params for the Mozart request + exclude_list = ["username", "get_job_details"] + filtered_query_params = {k: v for k, v in params.items() if k not in exclude_list and v is not None} + try: logging.info("Finding jobs for user: {}".format(username)) - # get list of jobs ids for the user - response = hysds.get_mozart_jobs(username=username, offset=offset, page_size=page_size) + # Get list of jobs ids for the user + response = hysds.get_mozart_jobs(username, **filtered_query_params) job_list = response.get("result") logging.info("Found Jobs: {}".format(job_list)) - #if settings.HYSDS_VERSION == "v4.0": - # get job info per job - job_list = hysds.get_jobs_info(x.get("id") for x in job_list) + + if get_job_details: + # Get job info per job + job_list = hysds.get_jobs_info(x.get("id") for x in job_list) + response_body = dict() response_body["code"] = status.HTTP_200_OK response_body["jobs"] = job_list diff --git a/api/utils/hysds_util.py b/api/utils/hysds_util.py index 4c2255b..97a8a71 100644 --- a/api/utils/hysds_util.py +++ b/api/utils/hysds_util.py @@ -624,21 +624,52 @@ def get_mozart_queues(): raise Exception("Couldn't get list of available queues") -def get_mozart_jobs(username, page_size=10, offset=0): +def get_mozart_jobs(username, + end_time=None, + job_type=None, + offset=0, + page_size=10, + priority=None, + queue=None, + start_time=None, + status=None, + tag=None + ): + """ + Returns mozart's job list + :param username: Username + :param page_size: Page size for pagination + :param offset: Offset for pagination + :param status: Job status + :param end_time: End time + :param start_time: Start time + :param priority: Job priority + :param queue: Queue + :param tag: User tag + :param job_type: Algorithm type + :return: Job list """ - Returns mozart's job list - :param username: - :param page_size: - :param offset: - :return: - """ - params = dict() - params["page_size"] = page_size - params["offset"] = offset # this is specifies the offset + params = { + k: v + for k, v in ( + ("end_time", end_time), + ("job_type", job_type), + ("offset", offset), + ("page_size", page_size), + ("priority", priority), + ("queue", queue), + ("start_time", start_time), + ("status", status), + ("tag", tag), + ) + if v is not None + } session = requests.Session() session.verify = False + logging.debug("Job params: {}".format(params)) + try: param_list = "" for key, value in params.items(): diff --git a/api/utils/ogc_translate.py b/api/utils/ogc_translate.py index 0bc1ea7..6be1c83 100644 --- a/api/utils/ogc_translate.py +++ b/api/utils/ogc_translate.py @@ -9,6 +9,19 @@ "ows": "http://www.opengis.net/ows/2.0" } +# This list extends the recommended WPS job statuses to include statuses 'Deduped', 'Deleted', and 'Offline'. +WPS_STATUSES = ["Accepted", "Running", "Succeeded", "Failed", "Dismissed", "Deduped", "Deleted", "Offline"] +HYSDS_STATUSES= ["job-started", "job-completed", "job-queued", "job-failed", "job-deduped", "job-revoked", "job-offline"] + +WPS_TO_HYSDS_JOB_STATUS_MAP = { + "Accepted": "job-queued", + "Running": "job-started", + "Succeeded": "job-completed", + "Failed": "job-failed", + "Dismissed": "job-revoked", + "Deduped": "job-deduped", + "Deleted": None, + "Offline": "job-offline"} def set_namespaces(xml_element): xml_element.set("xmlns:wps", "http://www.opengis.net/wps/2.0") @@ -18,6 +31,21 @@ def set_namespaces(xml_element): return xml_element +def get_hysds_status_from_wps(wps_status): + """ + Translate WPS job status to HySDS job status + + NOTE: HySDS does not support job status 'Deleted' and this status is not one of the base WPS job statuses. + Treating this status as a pass-through until there is consensus on HySDS-WPS mapping. + + :param status: (str) WPS job status + :return status: (str) HySDS job status + """ + if wps_status not in WPS_TO_HYSDS_JOB_STATUS_MAP: + statuses = ", ".join(str(status) for status in WPS_STATUSES) + raise ValueError("Invalid WPS status: {}. Valid values are: {}".format(wps_status, statuses)) + return WPS_TO_HYSDS_JOB_STATUS_MAP.get(wps_status) + def get_status(job_status, wps_compliant=False): """ From 94daabb2cf4b294f63d1f236aca7cd3fdfbc680a Mon Sep 17 00:00:00 2001 From: marjo-luc Date: Tue, 6 Aug 2024 19:43:38 +0000 Subject: [PATCH 04/27] /version 4.1.0a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 192978e..3af6287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a1" +version = "4.1.0a2" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 1cda9b2939ccadc6c7ef1a8a7dfd82f6833f872b Mon Sep 17 00:00:00 2001 From: bsatoriu <27687558+bsatoriu@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:27:01 -0700 Subject: [PATCH 05/27] Update members.py (#120) --- api/endpoints/members.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/api/endpoints/members.py b/api/endpoints/members.py index a54fe70..f36dc6e 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -525,20 +525,28 @@ def get(self, endpoint_uri): maap_user = get_authorized_user() if maap_user is None: - return Response('Unauthorized', status=401) + return Response('Unauthorized', status=status.HTTP_401_UNAUTHORIZED) else: - json_response = get_edc_credentials(endpoint_uri=endpoint_uri, user_id=maap_user.id) + edc_response = get_edc_credentials(endpoint_uri=endpoint_uri, user_id=maap_user.id) + + try: + edc_response_json = json.loads(edc_response) + response = jsonify( + accessKeyId=edc_response_json['accessKeyId'], + secretAccessKey=edc_response_json['secretAccessKey'], + sessionToken=edc_response_json['sessionToken'], + expiration=edc_response_json['expiration'] + ) - response = jsonify( - accessKeyId=json_response['accessKeyId'], - secretAccessKey=json_response['secretAccessKey'], - sessionToken=json_response['sessionToken'], - expiration=json_response['expiration'] - ) + response.headers.add('Access-Control-Allow-Origin', '*') - response.headers.add('Access-Control-Allow-Origin', '*') + return response - return response + except ValueError as ex: + response_body = dict() + response_body["code"] = status.HTTP_500_INTERNAL_SERVER_ERROR + response_body["message"] = edc_response.decode("utf-8") + return response_body, status.HTTP_500_INTERNAL_SERVER_ERROR @ns.route('/self/awsAccess/workspaceBucket') @@ -669,7 +677,7 @@ def get_edc_credentials(endpoint_uri, user_id): else: edl_response = edl_federated_request(url=endpoint) - return json.loads(edl_response.content) + return edl_response.content @ns.route('/pre-approved') From 096229a4de7c84e117ab37dd7a77e4610b928a50 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Wed, 14 Aug 2024 20:27:48 +0000 Subject: [PATCH 06/27] /version 4.1.0a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3af6287..5771887 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a2" +version = "4.1.0a3" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From b2c8836e6802cb9d05b0c61ccafb31e81b9c04c4 Mon Sep 17 00:00:00 2001 From: bsatoriu <27687558+bsatoriu@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:34:49 -0700 Subject: [PATCH 07/27] Remove {username} param from DPS job list endpoint (#132) * Add /dps/job/list method; deprecate /dps/job/{username}/list * Add /dps/job/list method; deprecate /dps/job/{username}/list * Add role constants --------- Co-authored-by: bsatoriu --- api/{cas => auth}/__init__.py | 0 api/{cas => auth}/cas_auth.py | 94 +------------------------ api/{cas => auth}/cas_urls.py | 0 api/auth/security.py | 100 +++++++++++++++++++++++++++ api/endpoints/algorithm.py | 2 +- api/endpoints/cmr.py | 2 +- api/endpoints/job.py | 115 +++++++++++++++++++++++-------- api/endpoints/members.py | 2 +- api/maapapp.py | 2 +- api/models/member.py | 14 +++- api/models/role.py | 18 +++++ api/schemas/member_job_schema.py | 2 +- api/schemas/role_schema.py | 11 +++ api/settings.py | 2 +- 14 files changed, 235 insertions(+), 129 deletions(-) rename api/{cas => auth}/__init__.py (100%) rename api/{cas => auth}/cas_auth.py (71%) rename api/{cas => auth}/cas_urls.py (100%) create mode 100644 api/auth/security.py create mode 100644 api/models/role.py create mode 100644 api/schemas/role_schema.py diff --git a/api/cas/__init__.py b/api/auth/__init__.py similarity index 100% rename from api/cas/__init__.py rename to api/auth/__init__.py diff --git a/api/cas/cas_auth.py b/api/auth/cas_auth.py similarity index 71% rename from api/cas/cas_auth.py rename to api/auth/cas_auth.py index 056244b..5b068c3 100644 --- a/api/cas/cas_auth.py +++ b/api/auth/cas_auth.py @@ -2,7 +2,7 @@ import flask import requests -from flask import abort, request, Response, json +from flask import request, json from flask_api import status from xmltodict import parse from flask import current_app @@ -10,9 +10,7 @@ from api.maap_database import db from api.models.member import Member from api.models.member_session import MemberSession -from api.models.member_job import MemberJob from api import settings -from functools import wraps from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto import Random @@ -21,7 +19,6 @@ from api.utils.url_util import proxied_url import ast - try: from urllib import urlopen except ImportError: @@ -31,7 +28,6 @@ PROXY_TICKET_PREFIX = "PGT-" - def validate(service, ticket): """ Will attempt to validate the ticket. If validation fails False @@ -132,15 +128,6 @@ def validate_bearer(token): return None -def validate_cas_request(token): - """ - Will attempt to validate a CAS machine token. Return True if validation succeeds. - """ - - current_app.logger.debug("validating cas request token {0}".format(token)) - return token == settings.CAS_SECRET_KEY - - def validate_cas_request(cas_url): xml_from_dict = {} @@ -214,82 +201,3 @@ def decrypt_proxy_ticket(ticket): return '' -def get_authorized_user(): - if 'proxy-ticket' in request.headers: - member_session = validate_proxy(request.headers['proxy-ticket']) - - if member_session is not None: - return member_session.member - - if 'Authorization' in request.headers: - bearer = request.headers.get('Authorization') - token = bearer.split()[1] - authorized = validate_bearer(token) - - if authorized is not None: - return authorized - - return None - - -def login_required(wrapped_function): - @wraps(wrapped_function) - def wrap(*args, **kwargs): - - if 'proxy-ticket' in request.headers: - authorized = validate_proxy(request.headers['proxy-ticket']) - - if authorized is not None: - return wrapped_function(*args, **kwargs) - - if 'cpticket' in request.headers: - authorized = validate_proxy(request.headers['cpticket']) - - if authorized is not None: - return wrapped_function(*args, **kwargs) - - if 'Authorization' in request.headers: - bearer = request.headers.get('Authorization') - token = bearer.split()[1] - authorized = validate_bearer(token) - - if authorized is not None: - return wrapped_function(*args, **kwargs) - - if 'cas-authorization' in request.headers: - authorized = validate_cas_request(request.headers['cas-authorization']) - - if authorized: - return wrapped_function(*args, **kwargs) - - if 'dps-token' in request.headers and valid_dps_request(): - return wrapped_function(*args, **kwargs) - - abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") - - return wrap - - -def valid_dps_request(): - if 'dps-token' in request.headers: - return settings.DPS_MACHINE_TOKEN == request.headers['dps-token'] - return False - - -def edl_federated_request(url, stream_response=False): - s = requests.Session() - response = s.get(url, stream=stream_response) - - if response.status_code == status.HTTP_401_UNAUTHORIZED: - maap_user = get_authorized_user() - - if maap_user is not None: - urs_token = db.session.query(Member).filter_by(id=maap_user.id).first().urs_token - s.headers.update({'Authorization': f'Bearer {urs_token},Basic {settings.MAAP_EDL_CREDS}', - 'Connection': 'close'}) - - response = s.get(url=response.url, stream=stream_response) - - return response - - diff --git a/api/cas/cas_urls.py b/api/auth/cas_urls.py similarity index 100% rename from api/cas/cas_urls.py rename to api/auth/cas_urls.py diff --git a/api/auth/security.py b/api/auth/security.py new file mode 100644 index 0000000..5380a55 --- /dev/null +++ b/api/auth/security.py @@ -0,0 +1,100 @@ +from functools import wraps +import requests +from flask import request, abort +from flask_api import status +from api import settings +from api.auth.cas_auth import validate_proxy, validate_bearer, validate_cas_request +from api.maap_database import db +from api.models.member import Member + +HEADER_PROXY_TICKET = "proxy-ticket" +HEADER_CP_TICKET = "cpticket" +HEADER_AUTHORIZATION = "Authorization" +HEADER_CAS_AUTHORIZATION = "cas-authorization" +HEADER_DPS_TOKEN = "dps-token" + + +def get_authorized_user(): + auth = get_auth_header() + + if auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET: + member_session = validate_proxy(request.headers[auth]) + + if member_session is not None: + return member_session.member + + if auth == HEADER_AUTHORIZATION: + bearer = request.headers.get(auth) + token = bearer.split()[1] + authorized = validate_bearer(token) + + if authorized is not None: + return authorized + + return None + + +def login_required(wrapped_function): + @wraps(wrapped_function) + def wrap(*args, **kwargs): + + auth = get_auth_header() + + if ((auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET) and + validate_proxy(request.headers[auth]) is not None): + return wrapped_function(*args, **kwargs) + + if auth == HEADER_AUTHORIZATION: + bearer = request.headers.get(auth) + token = bearer.split()[1] + authorized = validate_bearer(token) + + if authorized is not None: + return wrapped_function(*args, **kwargs) + + if auth == HEADER_CAS_AUTHORIZATION and validate_cas_request(request.headers[auth]) is not None: + return wrapped_function(*args, **kwargs) + + if auth == HEADER_DPS_TOKEN and valid_dps_request(): + return wrapped_function(*args, **kwargs) + + abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") + + return wrap + + +def valid_dps_request(): + if HEADER_DPS_TOKEN in request.headers: + return settings.DPS_MACHINE_TOKEN == request.headers[HEADER_DPS_TOKEN] + return False + + +def get_auth_header(): + if HEADER_PROXY_TICKET in request.headers: + return HEADER_PROXY_TICKET + if HEADER_CP_TICKET in request.headers: + return HEADER_CP_TICKET + if HEADER_AUTHORIZATION in request.headers: + return HEADER_AUTHORIZATION + if HEADER_CAS_AUTHORIZATION in request.headers: + return HEADER_CAS_AUTHORIZATION + if HEADER_DPS_TOKEN in request.headers: + return HEADER_DPS_TOKEN + return None + + +def edl_federated_request(url, stream_response=False): + s = requests.Session() + response = s.get(url, stream=stream_response) + + if response.status_code == status.HTTP_401_UNAUTHORIZED: + maap_user = get_authorized_user() + + if maap_user is not None: + urs_token = db.session.query(Member).filter_by(id=maap_user.id).first().urs_token + s.headers.update({'Authorization': f'Bearer {urs_token},Basic {settings.MAAP_EDL_CREDS}', + 'Connection': 'close'}) + + response = s.get(url=response.url, stream=stream_response) + + return response diff --git a/api/endpoints/algorithm.py b/api/endpoints/algorithm.py index c5023fb..92e98c6 100644 --- a/api/endpoints/algorithm.py +++ b/api/endpoints/algorithm.py @@ -11,7 +11,7 @@ import api.utils.hysds_util as hysds import api.settings as settings import api.utils.ogc_translate as ogc -from api.cas.cas_auth import get_authorized_user, login_required +from api.auth.security import get_authorized_user, login_required from api.maap_database import db from api.models.member_algorithm import MemberAlgorithm from sqlalchemy import or_, and_ diff --git a/api/endpoints/cmr.py b/api/endpoints/cmr.py index 0a184ef..861bd18 100755 --- a/api/endpoints/cmr.py +++ b/api/endpoints/cmr.py @@ -9,7 +9,7 @@ from flask_restx import Resource from flask_api import status from api.restplus import api -from api.cas.cas_auth import get_authorized_user, edl_federated_request +from api.auth.security import get_authorized_user, edl_federated_request from api.maap_database import db from api.models.member import Member from urllib import parse diff --git a/api/endpoints/job.py b/api/endpoints/job.py index ae9e835..bc93de8 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -14,14 +14,12 @@ import os import requests import traceback -from api.cas.cas_auth import get_authorized_user, login_required +from api.auth.security import get_authorized_user, login_required from api.maap_database import db from api.models.member_job import MemberJob from api.models.member import Member -from sqlalchemy import or_, and_ from datetime import datetime -from xml.etree.ElementTree import Element, SubElement, Comment, tostring, fromstring -import uuid +from xml.etree.ElementTree import Element, SubElement, tostring, fromstring log = logging.getLogger(__name__) @@ -452,7 +450,7 @@ def get(self, job_id): status=status.HTTP_500_INTERNAL_SERVER_ERROR) -@ns.route('/job//list') +@ns.route('/job/list') class Jobs(Resource): parser = api.parser() parser.add_argument('page_size', required=False, type=str, help="Job Listing Pagination Size") @@ -463,17 +461,18 @@ class Jobs(Resource): parser.add_argument('priority', type=int, help="Job priority, 0-9", required=False) parser.add_argument('start_time', type=str, help="Start time of @timestamp field", required=False) parser.add_argument('end_time', type=str, help="Start time of @timestamp field", required=False) - parser.add_argument('get_job_details', type=bool, help="Return full details if True. List of job id's if false. Default True.", required=False) - parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", required=False) - + parser.add_argument('get_job_details', type=bool, help="Return full details if True. " + "List of job id's if false. Default True.", required=False) + parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", + required=False) + parser.add_argument('username', required=False, type=str, help="Username of job submitter") @api.doc(security='ApiKeyAuth') @login_required - def get(self, username): + def get(self): """ - Returns a list of jobs for a given user. + Returns a list of jobs for a given user - :param username: Username :param get_job_details: Boolean that returns job details if set to True or just job ID's if set to False. Default is True. :param page_size: Page size for pagination :param offset: Offset for pagination @@ -484,33 +483,42 @@ def get(self, username): :param queue: Queue :param tag: User tag :param job_type: Algorithm type + :param username: Username :return: List of jobs for a given user that matches query params provided """ - + defaults = { - "username" : username, - "get_job_details" : True, # To preserve existing behavior, set default to True. In the future, we should set this to False. - "page_size" : 10, - "offset" : 0, - "status" : None, - "end_time" : None, - "start_time" : None, - "priority" : None, - "queue" : None, - "tag" : None, - "job_type" : None + "username": None, + "get_job_details": True, + # To preserve existing behavior, set default to True. In the future, we should set this to False. + "page_size": 10, + "offset": 0, + "status": None, + "end_time": None, + "start_time": None, + "priority": None, + "queue": None, + "tag": None, + "job_type": None } - + # Get params and set default values params = {key: request.args.get(key, default) for key, default in defaults.items()} + user = get_authorized_user() + username = user.username + + # Allow the username to be changed for admin roles using the username param + if user.is_admin() and params['username'] is not None: + username = params['username'] + is_param_true = lambda x: x if isinstance(x, bool) else x.lower() == 'true' get_job_details = is_param_true(params['get_job_details']) # If status is provided, make sure it is HySDS-compliant if params['status'] is not None: params['status'] = ogc.get_hysds_status_from_wps(params['status']) - + # Filter out the non-query params for the Mozart request exclude_list = ["username", "get_job_details"] filtered_query_params = {k: v for k, v in params.items() if k not in exclude_list and v is not None} @@ -521,7 +529,7 @@ def get(self, username): response = hysds.get_mozart_jobs(username, **filtered_query_params) job_list = response.get("result") logging.info("Found Jobs: {}".format(job_list)) - + if get_job_details: # Get job info per job job_list = hysds.get_jobs_info(x.get("id") for x in job_list) @@ -548,11 +556,62 @@ def get(self, username): except Exception as ex: return Response(ogc.get_exception(type="FailedGetJobs", origin_process="GetJobs", ex_message="Failed to get jobs for user {}. " \ - " please contact administrator " \ - "of DPS".format(username)), mimetype='text/xml', + " please contact administrator " \ + "of DPS".format(username)), mimetype='text/xml', status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@ns.route( + '/job//list', + doc={ + "deprecated": True, + }) +class JobsByUser(Resource): + parser = api.parser() + parser.add_argument('page_size', required=False, type=str, help="Job Listing Pagination Size") + parser.add_argument('offset', required=False, type=str, help="Job Listing Pagination Offset") + parser.add_argument('job_type', type=str, help="Job type + version, e.g. topsapp:v1.0", required=False) + parser.add_argument('tag', type=str, help="User-defined job tag", required=False) + parser.add_argument('queue', type=str, help="Submitted job queue", required=False) + parser.add_argument('priority', type=int, help="Job priority, 0-9", required=False) + parser.add_argument('start_time', type=str, help="Start time of @timestamp field", required=False) + parser.add_argument('end_time', type=str, help="Start time of @timestamp field", required=False) + parser.add_argument('get_job_details', type=bool, help="Return full details if True. " + "List of job id's if false. Default True.", required=False) + parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", required=False) + + @api.doc(security='ApiKeyAuth') + @login_required + def get(self, username): + """ + Returns a list of jobs for a given user + + :param username: Username + :param get_job_details: Boolean that returns job details if set to True or just job ID's if set to False. Default is True. + :param page_size: Page size for pagination + :param offset: Offset for pagination + :param status: Job status + :param end_time: End time + :param start_time: Start time + :param priority: Job priority + :param queue: Queue + :param tag: User tag + :param job_type: Algorithm type + :return: List of jobs for a given user that matches query params provided + """ + + # Check if username matches authenticated account + user = get_authorized_user() + if user is None or user.username != username: + return Response(ogc.get_exception(type="FailedGetJobs", origin_process="GetJobs", + ex_message="Failed to get jobs for user {}. " + "Forbidden request".format(username)), mimetype='text/xml', + status=status.HTTP_403_FORBIDDEN) + + # Wrap the Jobs class get method + return Jobs().get() + + @ns.route('/job/cancel/') class StopJobs(Resource): parser = api.parser() diff --git a/api/endpoints/members.py b/api/endpoints/members.py index f36dc6e..a47c952 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -5,7 +5,7 @@ from flask_api import status from api.restplus import api import api.settings as settings -from api.cas.cas_auth import get_authorized_user, login_required, edl_federated_request, valid_dps_request +from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request from api.maap_database import db from api.utils import github_util from api.models.member import Member as Member_db diff --git a/api/maapapp.py b/api/maapapp.py index c33b664..bc05097 100755 --- a/api/maapapp.py +++ b/api/maapapp.py @@ -5,7 +5,7 @@ import os from flask import Flask, Blueprint, request, session from api import settings -from api.cas.cas_auth import validate +from api.auth.cas_auth import validate from api.utils.environments import Environments, get_environment from api.utils.url_util import proxied_url from api.endpoints.cmr import ns as cmr_collections_namespace diff --git a/api/models/member.py b/api/models/member.py index 22ac9e8..fec26c9 100644 --- a/api/models/member.py +++ b/api/models/member.py @@ -1,6 +1,6 @@ from api.models import Base from api.maap_database import db - +from api.models.role import Role class Member(Base): __tablename__ = 'member' @@ -8,6 +8,7 @@ class Member(Base): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(), unique=True) email = db.Column(db.String(), unique=True) + role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False, default=1) first_name = db.Column(db.String()) last_name = db.Column(db.String()) organization = db.Column(db.String()) @@ -22,7 +23,16 @@ class Member(Base): creation_date = db.Column(db.DateTime()) def get_display_name(self): - return "{} {}".format(self.first_name,self.last_name) + return "{} {}".format(self.first_name, self.last_name) + + def is_guest(self): + return self.role_id == Role.ROLE_GUEST + + def is_member(self): + return self.role_id == Role.ROLE_MEMBER + + def is_admin(self): + return self.role_id == Role.ROLE_ADMIN def __repr__(self): return "".format(self=self) diff --git a/api/models/role.py b/api/models/role.py new file mode 100644 index 0000000..6a5b211 --- /dev/null +++ b/api/models/role.py @@ -0,0 +1,18 @@ +from api.models import Base +from api.maap_database import db + + +class Role(Base): + __tablename__ = 'role' + + ROLE_GUEST = 1 + ROLE_MEMBER = 2 + ROLE_ADMIN = 3 + + id = db.Column(db.Integer, primary_key=True) + role_name = db.Column(db.String()) + + def __repr__(self): + return "".format(self=self) + + diff --git a/api/schemas/member_job_schema.py b/api/schemas/member_job_schema.py index f485ad5..bbf3f03 100644 --- a/api/schemas/member_job_schema.py +++ b/api/schemas/member_job_schema.py @@ -6,4 +6,4 @@ class MemberJobSchema(SQLAlchemyAutoSchema): class Meta: model = MemberJob include_fk = True - load_instance = True \ No newline at end of file + load_instance = True diff --git a/api/schemas/role_schema.py b/api/schemas/role_schema.py new file mode 100644 index 0000000..877c941 --- /dev/null +++ b/api/schemas/role_schema.py @@ -0,0 +1,11 @@ +from api.models.role import Role +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class RoleSchema(SQLAlchemyAutoSchema): + class Meta: + model = Role + include_relationships = True + load_instance = True + + diff --git a/api/settings.py b/api/settings.py index eaee2d9..d5fb593 100755 --- a/api/settings.py +++ b/api/settings.py @@ -97,7 +97,7 @@ def str2bool(v): AWS_REQUESTER_PAYS_BUCKET_ARN = os.getenv('AWS_REQUESTER_PAYS_BUCKET_ARN', 'arn:aws:iam::???:role/???') # DB -DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://maapuser:mysecretpassword@db/maap') +DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://maapuser:mysecretpassword@localhost/maap') # SMTP SMTP_HOSTNAME = os.getenv('SMTP_HOSTNAME', 'my_smtp_hostname') From ce6a216ce06f51f9516e69c26c0c36bfac4914e2 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Wed, 14 Aug 2024 21:35:25 +0000 Subject: [PATCH 08/27] /version 4.1.0a4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5771887..35a335d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a3" +version = "4.1.0a4" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 1d0e59ccc91fc0019b559538f882d3ccc62ffd9a Mon Sep 17 00:00:00 2001 From: bsatoriu <27687558+bsatoriu@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:53:58 -0700 Subject: [PATCH 09/27] User secret management (#135) * Add user secrets endpoints * Edit secret deletion error message Co-authored-by: Sujen Shah * Restrict login_required decorator to specific roles; Improved permissions error handling; minor refactoring --------- Co-authored-by: bsatoriu Co-authored-by: Sujen Shah --- api/auth/security.py | 45 +- api/endpoints/admin.py | 97 +++ api/endpoints/algorithm.py | 9 +- api/endpoints/job.py | 8 +- api/endpoints/members.py | 277 ++++--- api/maapapp.py | 2 + api/models/member.py | 2 +- api/models/member_secret.py | 16 + api/schemas/member_secret_schema.py | 9 + api/settings.py | 1 + api/utils/http_util.py | 10 + poetry.lock | 1141 +++++++++++++++------------ pyproject.toml | 1 + 13 files changed, 972 insertions(+), 646 deletions(-) create mode 100755 api/endpoints/admin.py create mode 100644 api/models/member_secret.py create mode 100644 api/schemas/member_secret_schema.py create mode 100644 api/utils/http_util.py diff --git a/api/auth/security.py b/api/auth/security.py index 5380a55..33e88dc 100644 --- a/api/auth/security.py +++ b/api/auth/security.py @@ -6,12 +6,15 @@ from api.auth.cas_auth import validate_proxy, validate_bearer, validate_cas_request from api.maap_database import db from api.models.member import Member +from api.models.role import Role HEADER_PROXY_TICKET = "proxy-ticket" HEADER_CP_TICKET = "cpticket" HEADER_AUTHORIZATION = "Authorization" HEADER_CAS_AUTHORIZATION = "cas-authorization" HEADER_DPS_TOKEN = "dps-token" +MEMBER_STATUS_ACTIVE = "active" +MEMBER_STATUS_SUSPENDED = "suspended" def get_authorized_user(): @@ -34,34 +37,36 @@ def get_authorized_user(): return None -def login_required(wrapped_function): - @wraps(wrapped_function) - def wrap(*args, **kwargs): +def login_required(role=Role.ROLE_GUEST): + def login_required_outer(wrapped_function): + @wraps(wrapped_function) + def wrap(*args, **kwargs): + auth = get_auth_header() - auth = get_auth_header() + if auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET: + member_session = validate_proxy(request.headers[auth]) - if ((auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET) and - validate_proxy(request.headers[auth]) is not None): - return wrapped_function(*args, **kwargs) + if member_session is not None and member_session.member.role_id >= role: + return wrapped_function(*args, **kwargs) - if auth == HEADER_AUTHORIZATION: - bearer = request.headers.get(auth) - token = bearer.split()[1] - authorized = validate_bearer(token) + if auth == HEADER_AUTHORIZATION: + bearer = request.headers.get(auth) + token = bearer.split()[1] + authorized = validate_bearer(token) - if authorized is not None: - return wrapped_function(*args, **kwargs) - - if auth == HEADER_CAS_AUTHORIZATION and validate_cas_request(request.headers[auth]) is not None: - return wrapped_function(*args, **kwargs) + if authorized is not None: + return wrapped_function(*args, **kwargs) - if auth == HEADER_DPS_TOKEN and valid_dps_request(): - return wrapped_function(*args, **kwargs) + if auth == HEADER_CAS_AUTHORIZATION and validate_cas_request(request.headers[auth]) is not None: + return wrapped_function(*args, **kwargs) - abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") + if auth == HEADER_DPS_TOKEN and valid_dps_request(): + return wrapped_function(*args, **kwargs) - return wrap + abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") + return wrap + return login_required_outer def valid_dps_request(): if HEADER_DPS_TOKEN in request.headers: diff --git a/api/endpoints/admin.py b/api/endpoints/admin.py new file mode 100755 index 0000000..77afed6 --- /dev/null +++ b/api/endpoints/admin.py @@ -0,0 +1,97 @@ +import logging +from flask_restx import Resource +from flask import request +from flask_api import status +from api.models.role import Role +from api.restplus import api +from api.auth.security import login_required +from api.maap_database import db +from api.models.pre_approved import PreApproved +from api.schemas.pre_approved_schema import PreApprovedSchema +from datetime import datetime +import json + +from api.utils.http_util import err_response + +log = logging.getLogger(__name__) +ns = api.namespace('admin', description='Operations related to the MAAP admin') + +@ns.route('/pre-approved') +class PreApprovedEmails(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def get(self): + pre_approved = db.session.query( + PreApproved.email, + PreApproved.creation_date + ).order_by(PreApproved.email).all() + + pre_approved_schema = PreApprovedSchema() + result = [json.loads(pre_approved_schema.dumps(p)) for p in pre_approved] + return result + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def post(self): + + """ + Create new pre-approved email. Wildcards are supported for starting email characters. + + Format of JSON to post: + { + "email": "" + } + + Sample 1. Any email ending in "@maap-project.org" is pre-approved + { + "email": "*@maap-project.org" + } + + Sample 2. Any email matching "jane.doe@maap-project.org" is pre-approved + { + "email": "jane.doe@maap-project.org" + } + """ + + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + email = req_data.get("email", "") + if not isinstance(email, str) or not email: + return err_response("Valid email is required.") + + pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first() + + if pre_approved_email is not None: + return err_response(msg="Email already exists") + + new_email = PreApproved(email=email, creation_date=datetime.utcnow()) + + db.session.add(new_email) + db.session.commit() + + pre_approved_schema = PreApprovedSchema() + return json.loads(pre_approved_schema.dumps(new_email)) + + +@ns.route('/pre-approved/') +class PreApprovedEmails(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def delete(self, email): + """ + Delete pre-approved email + """ + + pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first() + + if pre_approved_email is None: + return err_response(msg="Email does not exist") + + db.session.query(PreApproved).filter_by(email=email).delete() + db.session.commit() + + return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(email)} diff --git a/api/endpoints/algorithm.py b/api/endpoints/algorithm.py index 92e98c6..9ec7110 100644 --- a/api/endpoints/algorithm.py +++ b/api/endpoints/algorithm.py @@ -3,9 +3,10 @@ from flask import request, Response from flask_restx import Resource, reqparse from flask_api import status + +from api.models.member import Member from api.restplus import api import re -import json import traceback import api.utils.github_util as git import api.utils.hysds_util as hysds @@ -91,7 +92,7 @@ class Register(Resource): ]""") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This will create the hysds spec files and commit to git @@ -422,7 +423,7 @@ def get(self, algo_id): .format(ex, tb)), status=status.HTTP_500_INTERNAL_SERVER_ERROR, mimetype='text/xml') @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def delete(self, algo_id): """ delete a registered algorithm @@ -494,7 +495,7 @@ def post(self): class Publish(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This endpoint is called by a logged-in user to make an algorithm public diff --git a/api/endpoints/job.py b/api/endpoints/job.py index bc93de8..0e7474b 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -30,7 +30,7 @@ class Submit(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This will submit jobs to the Job Execution System (HySDS) @@ -468,7 +468,7 @@ class Jobs(Resource): parser.add_argument('username', required=False, type=str, help="Username of job submitter") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): """ Returns a list of jobs for a given user @@ -581,7 +581,7 @@ class JobsByUser(Resource): parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", required=False) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, username): """ Returns a list of jobs for a given user @@ -619,7 +619,7 @@ class StopJobs(Resource): help="Wait for Cancel job to finish") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, job_id): # TODO: add optional parameter wait_for_completion to wait for cancel job to complete. # Since this can take a long time, we don't wait by default. diff --git a/api/endpoints/members.py b/api/endpoints/members.py index a47c952..b1c576c 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -3,48 +3,44 @@ from flask_restx import Resource, reqparse from flask import request, jsonify, Response from flask_api import status +from sqlalchemy.exc import SQLAlchemyError from api.restplus import api import api.settings as settings -from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request +from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request, \ + MEMBER_STATUS_ACTIVE, MEMBER_STATUS_SUSPENDED from api.maap_database import db from api.utils import github_util from api.models.member import Member as Member_db from api.models.member_session import MemberSession as MemberSession_db +from api.models.member_secret import MemberSecret as MemberSecret_db from api.schemas.member_schema import MemberSchema from api.schemas.member_session_schema import MemberSessionSchema from api.utils.email_util import send_user_status_update_active_user_email, \ send_user_status_update_suspended_user_email, send_user_status_change_email, \ send_welcome_to_maap_active_user_email, send_welcome_to_maap_suspended_user_email from api.models.pre_approved import PreApproved -from api.schemas.pre_approved_schema import PreApprovedSchema -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone import json import boto3 import requests from urllib import parse + +from api.utils.http_util import err_response, custom_response from api.utils.url_util import proxied_url +from cryptography.fernet import Fernet log = logging.getLogger(__name__) ns = api.namespace('members', description='Operations for MAAP members') s3_client = boto3.client('s3', region_name=settings.AWS_REGION) sts_client = boto3.client('sts', region_name=settings.AWS_REGION) - -STATUS_ACTIVE = "active" -STATUS_SUSPENDED = "suspended" - - -def err_response(msg, code=status.HTTP_400_BAD_REQUEST): - return { - 'code': code, - 'message': msg - }, code +fernet = Fernet(settings.FERNET_KEY) @ns.route('') class Member(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): members = db.session.query( Member_db.id, @@ -65,7 +61,7 @@ def get(self): class Member(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, key): cols = [ @@ -103,7 +99,7 @@ def get(self, key): pgt_result = json.loads(member_session_schema.dumps(pgt_ticket)) result = json.loads(json.dumps(dict(result.items() | pgt_result.items()))) - # If the requested user info belongs to the logged in user, + # If the requested user info belongs to the logged-in user, # also include additional ssh key information belonging to the user if member.username == key: cols = [ @@ -124,7 +120,7 @@ def get(self, key): return result @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, key): """ @@ -185,7 +181,7 @@ def post(self, key): (~PreApproved.email.like("*%") & PreApproved.email.like(email)) ).first() - status = STATUS_SUSPENDED if pre_approved_email is None else STATUS_ACTIVE + member_status = MEMBER_STATUS_SUSPENDED if pre_approved_email is None else MEMBER_STATUS_ACTIVE guest = Member_db(first_name=first_name, last_name=last_name, @@ -195,14 +191,14 @@ def post(self, key): public_ssh_key=req_data.get("public_ssh_key", None), public_ssh_key_modified_date=datetime.utcnow(), public_ssh_key_name=req_data.get("public_ssh_key_name", None), - status=status, + status=member_status, creation_date=datetime.utcnow()) db.session.add(guest) db.session.commit() # Send Email Notifications based on member status - if status == STATUS_ACTIVE: + if member_status == MEMBER_STATUS_ACTIVE: send_user_status_change_email(guest, True, True, proxied_url(request)) send_welcome_to_maap_active_user_email(guest, proxied_url(request)) else: @@ -213,7 +209,7 @@ def post(self, key): return json.loads(member_schema.dumps(guest)) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def put(self, key): """ @@ -277,7 +273,7 @@ def put(self, key): class MemberStatus(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, key): """ @@ -297,24 +293,24 @@ def post(self, key): if not isinstance(req_data, dict): return err_response("Valid JSON body object required.") - status = req_data.get("status", "") - if not isinstance(status, str) or not status: + member_status = req_data.get("status", "") + if not isinstance(member_status, str) or not member_status: return err_response("Valid status string required.") - if status != STATUS_ACTIVE and status != STATUS_SUSPENDED: - return err_response("Status must be either " + STATUS_ACTIVE + " or " + STATUS_SUSPENDED) + if member_status != MEMBER_STATUS_ACTIVE and member_status != MEMBER_STATUS_SUSPENDED: + return err_response("Status must be either " + MEMBER_STATUS_ACTIVE + " or " + MEMBER_STATUS_SUSPENDED) member = db.session.query(Member_db).filter_by(username=key).first() if member is None: return err_response(msg="No member found with key " + key, code=404) - old_status = member.status if member.status is not None else STATUS_SUSPENDED - activated = old_status == STATUS_SUSPENDED and status == STATUS_ACTIVE - deactivated = old_status == STATUS_ACTIVE and status == STATUS_SUSPENDED + old_status = member.status if member.status is not None else MEMBER_STATUS_SUSPENDED + activated = old_status == MEMBER_STATUS_SUSPENDED and member_status == MEMBER_STATUS_ACTIVE + deactivated = old_status == MEMBER_STATUS_ACTIVE and member_status == MEMBER_STATUS_SUSPENDED if activated or deactivated: - member.status = status + member.status = member_status db.session.commit() gitlab_account = github_util.sync_gitlab_account( activated, @@ -348,7 +344,7 @@ def post(self, key): class Self(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): authorized_user = get_authorized_user() @@ -378,23 +374,11 @@ def get(self): return member -@ns.route('/selfTest') -class SelfTest(Resource): - - @api.doc(security='ApiKeyAuth') - @login_required - def get(self): - member = get_authorized_user() - member_schema = MemberSchema() - - return json.loads(member_schema.dumps(member)) - - @ns.route('/self/sshKey') class PublicSshKeyUpload(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): if 'file' not in request.files: log.error('Upload attempt with no file') @@ -417,7 +401,7 @@ def post(self): return json.loads(member_schema.dumps(member)) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def delete(self): member = get_authorized_user() @@ -432,6 +416,120 @@ def delete(self): return json.loads(member_schema.dumps(member)) +@ns.route('/self/secrets') +class Secrets(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self): + + try: + secrets = \ + ( + db.session.query( + MemberSecret_db.secret_name, + MemberSecret_db.secret_value) + .filter_by(member_id=get_authorized_user().id) + .order_by(MemberSecret_db.secret_name) + .all()) + + result = [{ + 'secret_name': s.secret_name + } for s in secrets] + + return result + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) + + @api.doc(security='ApiKeyAuth') + @login_required() + def post(self): + + try: + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + secret_name = req_data.get("secret_name", "") + if not isinstance(secret_name, str) or not secret_name: + return err_response("secret_name is required.") + + secret_value = req_data.get("secret_value", "") + if not isinstance(secret_value, str) or not secret_value: + return err_response("secret_value is required.") + + member = get_authorized_user() + secret = db.session.query(MemberSecret_db).filter_by(member_id=member.id, secret_name=secret_name).first() + + if secret is not None: + return err_response(msg="Secret already exists with name {}. Please delete and re-create the secret to update it's value. ".format(secret_name)) + + encrypted_secret = fernet.encrypt(secret_value.encode()).decode("utf-8") + + new_secret = MemberSecret_db(member_id=member.id, + secret_name=secret_name, + secret_value=encrypted_secret, + creation_date=datetime.utcnow()) + + db.session.add(new_secret) + db.session.commit() + + return { + 'secret_name': secret_name + }, status.HTTP_200_OK + + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@ns.route('/self/secrets/') +class Secrets(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self, name): + + try: + secret = \ + ( + db.session.query( + MemberSecret_db.secret_name, + MemberSecret_db.secret_value) + .filter_by(member_id=get_authorized_user().id, secret_name=name) + .first()) + + if secret is None: + return err_response(msg="No secret exists with name {}".format(name), code=status.HTTP_404_NOT_FOUND) + + result = { + 'secret_name': secret.secret_name, + 'secret_value': fernet.decrypt(secret.secret_value).decode("utf-8") + } + + return result, status.HTTP_200_OK + + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) + + @api.doc(security='ApiKeyAuth') + @login_required() + def delete(self, name): + + try: + member = get_authorized_user() + secret = db.session.query(MemberSecret_db).filter_by(member_id=member.id, secret_name=name).first() + + if secret is None: + return err_response(msg="No secret exists with name " + name) + + db.session.delete(secret) + db.session.commit() + + return custom_response("Successfully deleted secret {}".format(name)) + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) + + @ns.route('/self/presignedUrlS3//') class PresignedUrlS3(Resource): expiration_param = reqparse.RequestParser() @@ -439,7 +537,7 @@ class PresignedUrlS3(Resource): expiration_param.add_argument('ws', type=str, required=False, default="") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() @api.expect(expiration_param) def get(self, bucket, key): @@ -482,7 +580,7 @@ class AwsAccessRequesterPaysBucket(Resource): expiration_param.add_argument('exp', type=int, required=False, default=60 * 60 * 12) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() @api.expect(expiration_param) def get(self): member = get_authorized_user() @@ -519,7 +617,7 @@ class AwsAccessEdcCredentials(Resource): """ @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, endpoint_uri): s = requests.Session() maap_user = get_authorized_user() @@ -561,7 +659,7 @@ class AwsAccessUserBucketCredentials(Resource): """ @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): maap_user = get_authorized_user() @@ -678,84 +776,3 @@ def get_edc_credentials(endpoint_uri, user_id): edl_response = edl_federated_request(url=endpoint) return edl_response.content - - -@ns.route('/pre-approved') -class PreApprovedEmails(Resource): - - @api.doc(security='ApiKeyAuth') - @login_required - def get(self): - pre_approved = db.session.query( - PreApproved.email, - PreApproved.creation_date - ).order_by(PreApproved.email).all() - - pre_approved_schema = PreApprovedSchema() - result = [json.loads(pre_approved_schema.dumps(p)) for p in pre_approved] - return result - - @api.doc(security='ApiKeyAuth') - @login_required - def post(self): - - """ - Create new pre-approved email. Wildcards are supported for starting email characters. - - Format of JSON to post: - { - "email": "" - } - - Sample 1. Any email ending in "@maap-project.org" is pre-approved - { - "email": "*@maap-project.org" - } - - Sample 2. Any email matching "jane.doe@maap-project.org" is pre-approved - { - "email": "jane.doe@maap-project.org" - } - """ - - req_data = request.get_json() - if not isinstance(req_data, dict): - return err_response("Valid JSON body object required.") - - email = req_data.get("email", "") - if not isinstance(email, str) or not email: - return err_response("Valid email is required.") - - pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first() - - if pre_approved_email is not None: - return err_response(msg="Email already exists") - - new_email = PreApproved(email=email, creation_date=datetime.utcnow()) - - db.session.add(new_email) - db.session.commit() - - pre_approved_schema = PreApprovedSchema() - return json.loads(pre_approved_schema.dumps(new_email)) - - -@ns.route('/pre-approved/') -class PreApprovedEmails(Resource): - - @api.doc(security='ApiKeyAuth') - @login_required - def delete(self, email): - """ - Delete pre-approved email - """ - - pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first() - - if pre_approved_email is None: - return err_response(msg="Email does not exist") - - db.session.query(PreApproved).filter_by(email=email).delete() - db.session.commit() - - return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(email)} diff --git a/api/maapapp.py b/api/maapapp.py index bc05097..fd4fcba 100755 --- a/api/maapapp.py +++ b/api/maapapp.py @@ -15,6 +15,7 @@ from api.endpoints.wms import ns as wms_namespace from api.endpoints.members import ns as members_namespace from api.endpoints.environment import ns as environment_namespace +from api.endpoints.admin import ns as admin_namespace from api.restplus import api from api.maap_database import db from api.models import initialize_sql @@ -102,6 +103,7 @@ def initialize_app(flask_app): api.add_namespace(wms_namespace) api.add_namespace(members_namespace) api.add_namespace(environment_namespace) + api.add_namespace(admin_namespace) flask_app.register_blueprint(blueprint) diff --git a/api/models/member.py b/api/models/member.py index fec26c9..b8221a3 100644 --- a/api/models/member.py +++ b/api/models/member.py @@ -8,7 +8,7 @@ class Member(Base): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(), unique=True) email = db.Column(db.String(), unique=True) - role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False, default=1) + role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False, default=Role.ROLE_GUEST) first_name = db.Column(db.String()) last_name = db.Column(db.String()) organization = db.Column(db.String()) diff --git a/api/models/member_secret.py b/api/models/member_secret.py new file mode 100644 index 0000000..fa37521 --- /dev/null +++ b/api/models/member_secret.py @@ -0,0 +1,16 @@ +from api.models import Base +from api.maap_database import db + + +class MemberSecret(Base): + __tablename__ = 'member_secret' + + id = db.Column(db.Integer, primary_key=True) + member_id = db.Column(db.Integer, db.ForeignKey('member.id'), nullable=False) + secret_name = db.Column(db.String()) + secret_value = db.Column(db.String()) + creation_date = db.Column(db.DateTime()) + member = db.relationship('Member', backref=db.backref('secrets')) + + def __repr__(self): + return "".format(self=self) \ No newline at end of file diff --git a/api/schemas/member_secret_schema.py b/api/schemas/member_secret_schema.py new file mode 100644 index 0000000..14bc64e --- /dev/null +++ b/api/schemas/member_secret_schema.py @@ -0,0 +1,9 @@ +from api.models.member_secret import MemberSecret +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class MemberSecretSchema(SQLAlchemyAutoSchema): + class Meta: + model = MemberSecret + include_fk = True + load_instance = True diff --git a/api/settings.py b/api/settings.py index d5fb593..c36e872 100755 --- a/api/settings.py +++ b/api/settings.py @@ -98,6 +98,7 @@ def str2bool(v): # DB DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://maapuser:mysecretpassword@localhost/maap') +FERNET_KEY = os.getenv('FERNET_KEY','') # SMTP SMTP_HOSTNAME = os.getenv('SMTP_HOSTNAME', 'my_smtp_hostname') diff --git a/api/utils/http_util.py b/api/utils/http_util.py new file mode 100644 index 0000000..66b1579 --- /dev/null +++ b/api/utils/http_util.py @@ -0,0 +1,10 @@ +from flask_api import status + +def custom_response(msg, code=status.HTTP_200_OK): + return { + 'code': code, + 'message': msg + }, code + +def err_response(msg, code=status.HTTP_400_BAD_REQUEST): + return custom_response(msg, code) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 26144a4..d0b9e48 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aniso8601" @@ -16,13 +16,13 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] name = "astroid" -version = "3.2.2" +version = "3.2.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, - {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, + {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, + {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, ] [package.dependencies] @@ -30,22 +30,22 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "blinker" @@ -60,17 +60,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.125" +version = "1.34.160" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.125-py3-none-any.whl", hash = "sha256:116d9eb3c26cf313a2e1e44ef704d1f98f9eb18e7628695d07b01b44a8683544"}, - {file = "boto3-1.34.125.tar.gz", hash = "sha256:31c4a5e4d6f9e6116be61ff654b424ddbd1afcdefe0e8b870c4796f9108eb1c6"}, + {file = "boto3-1.34.160-py3-none-any.whl", hash = "sha256:bf3153bf5d66be2bb2112edc94eb143c0cba3fb502c5591437bd1c54f57eb559"}, + {file = "boto3-1.34.160.tar.gz", hash = "sha256:79450f92188a8b992b3d0b802028acadf448bc6fdde877c3262c9f94d74d1c7d"}, ] [package.dependencies] -botocore = ">=1.34.125,<1.35.0" +botocore = ">=1.34.160,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -79,13 +79,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.125" +version = "1.34.160" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.125-py3-none-any.whl", hash = "sha256:71e97e7d2c088f1188ba6976441b5857a5425acd4aaa31b45d13119c9cb86424"}, - {file = "botocore-1.34.125.tar.gz", hash = "sha256:d2882be011ad5b16e7ab4a96360b5b66a0a7e175c1ea06dbf2de473c0a0a33d8"}, + {file = "botocore-1.34.160-py3-none-any.whl", hash = "sha256:39bcf31318a062a8a9260bf7044131694ed18f019568d2eba0a22164fdca49bd"}, + {file = "botocore-1.34.160.tar.gz", hash = "sha256:a5fd531c640fb2dc8b83f264efbb87a6e33b9c9f66ebbb1c61b42908f2786cac"}, ] [package.dependencies] @@ -97,30 +97,109 @@ urllib3 = [ ] [package.extras] -crt = ["awscrt (==0.20.11)"] +crt = ["awscrt (==0.21.2)"] [[package]] name = "cachetools" -version = "5.3.3" +version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] +[[package]] +name = "cffi" +version = "1.17.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -245,6 +324,55 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "43.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "dill" version = "0.3.8" @@ -350,6 +478,17 @@ files = [ flask = ">=2.2.5" sqlalchemy = ">=2.0.16" +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + [[package]] name = "gitdb" version = "4.0.11" @@ -487,22 +626,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.2.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, + {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +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)"] [[package]] name = "importlib-resources" @@ -577,13 +716,13 @@ files = [ [[package]] name = "jsonschema" -version = "4.22.0" +version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, ] [package.dependencies] @@ -594,7 +733,7 @@ rpds-py = ">=0.7.1" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-specifications" @@ -612,153 +751,149 @@ referencing = ">=0.31.0" [[package]] name = "lxml" -version = "5.2.2" +version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"}, - {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"}, - {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"}, - {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"}, - {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"}, - {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"}, - {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, - {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, - {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, - {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, - {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, - {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, - {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, - {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, - {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, - {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, - {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, - {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, - {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"}, - {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"}, - {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"}, - {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"}, - {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"}, - {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"}, - {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"}, - {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"}, - {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, - {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, - {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, - {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, - {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"}, - {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"}, - {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"}, - {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"}, - {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"}, - {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"}, - {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"}, - {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"}, - {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"}, - {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"}, - {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"}, - {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"}, - {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"}, - {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"}, - {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"}, - {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"}, - {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"}, - {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, - {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, - {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, - {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, - {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, - {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, ] [package.extras] @@ -766,21 +901,25 @@ cssselect = ["cssselect (>=0.7)"] html-clean = ["lxml-html-clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.10)"] +source = ["Cython (>=3.0.11)"] [[package]] name = "mapproxy" -version = "2.0.2" +version = "2.1.0" description = "An accelerating proxy for tile and web map services" optional = false python-versions = "*" files = [ - {file = "MapProxy-2.0.2-py2.py3-none-any.whl", hash = "sha256:dfd49739a2908c239355ef194eb962e75fc11ff08d34ac8f2927adcb1d79aa7d"}, - {file = "MapProxy-2.0.2.tar.gz", hash = "sha256:1f03b982faec5bda40af3e112edc4d7c29a216a6bce40022eb004923e17d184f"}, + {file = "MapProxy-2.1.0-py3-none-any.whl", hash = "sha256:dcf5c63dfc480528c3145f6872a846049069c3c40945fb736bdb0dae1551cf72"}, + {file = "MapProxy-2.1.0.tar.gz", hash = "sha256:45f03abfadb9802861a6a54a04abd052499745d9e55107f922124b473b2a0bd6"}, ] [package.dependencies] +future = "*" +importlib-resources = "*" +jsonschema = "*" Pillow = "<2.4.0 || >2.4.0,<8.3.0 || >8.3.0,<8.3.1 || >8.3.1" +pyproj = ">=2" PyYAML = ">=3.0" [[package]] @@ -948,84 +1087,95 @@ files = [ [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -1151,6 +1301,17 @@ files = [ {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pycryptodome" version = "3.20.0" @@ -1194,34 +1355,34 @@ files = [ [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.2.3" +version = "3.2.6" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.3-py3-none-any.whl", hash = "sha256:b3d7d2708a3e04b4679e02d99e72329a8b7ee8afb8d04110682278781f889fa8"}, - {file = "pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60"}, + {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, + {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +astroid = ">=3.2.4,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -1316,62 +1477,64 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -1426,121 +1589,125 @@ requests = ">=1.0.0" [[package]] name = "rpds-py" -version = "0.18.1" +version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, - {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, - {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, - {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, - {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, - {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, - {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, - {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, - {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, - {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, - {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, - {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, - {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] [[package]] name = "s3transfer" -version = "0.10.1" +version = "0.10.2" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, - {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, ] [package.dependencies] @@ -1573,64 +1740,64 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.30" +version = "2.0.32" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:955991a09f0992c68a499791a753523f50f71a6885531568404fa0f231832aa0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f69e4c756ee2686767eb80f94c0125c8b0a0b87ede03eacc5c8ae3b54b99dc46"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c9db1ce00e59e8dd09d7bae852a9add716efdc070a3e2068377e6ff0d6fdaa"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1429a4b0f709f19ff3b0cf13675b2b9bfa8a7e79990003207a011c0db880a13"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:efedba7e13aa9a6c8407c48facfdfa108a5a4128e35f4c68f20c3407e4376aa9"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16863e2b132b761891d6c49f0a0f70030e0bcac4fd208117f6b7e053e68668d0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win32.whl", hash = "sha256:2ecabd9ccaa6e914e3dbb2aa46b76dede7eadc8cbf1b8083c94d936bcd5ffb49"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win_amd64.whl", hash = "sha256:0b3f4c438e37d22b83e640f825ef0f37b95db9aa2d68203f2c9549375d0b2260"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a79d65395ac5e6b0c2890935bad892eabb911c4aa8e8015067ddb37eea3d56c"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a5baf9267b752390252889f0c802ea13b52dfee5e369527da229189b8bd592e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5a646930c5123f8461f6468901573f334c2c63c795b9af350063a736d0134"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296230899df0b77dec4eb799bcea6fbe39a43707ce7bb166519c97b583cfcab3"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c62d401223f468eb4da32627bffc0c78ed516b03bb8a34a58be54d618b74d472"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3b69e934f0f2b677ec111b4d83f92dc1a3210a779f69bf905273192cf4ed433e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win32.whl", hash = "sha256:77d2edb1f54aff37e3318f611637171e8ec71472f1fdc7348b41dcb226f93d90"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win_amd64.whl", hash = "sha256:b6c7ec2b1f4969fc19b65b7059ed00497e25f54069407a8701091beb69e591a5"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a8e3b0a7e09e94be7510d1661339d6b52daf202ed2f5b1f9f48ea34ee6f2d57"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b60203c63e8f984df92035610c5fb76d941254cf5d19751faab7d33b21e5ddc0"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1dc3eabd8c0232ee8387fbe03e0a62220a6f089e278b1f0aaf5e2d6210741ad"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:40ad017c672c00b9b663fcfcd5f0864a0a97828e2ee7ab0c140dc84058d194cf"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e42203d8d20dc704604862977b1470a122e4892791fe3ed165f041e4bf447a1b"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win32.whl", hash = "sha256:2a4f4da89c74435f2bc61878cd08f3646b699e7d2eba97144030d1be44e27584"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win_amd64.whl", hash = "sha256:b6bf767d14b77f6a18b6982cbbf29d71bede087edae495d11ab358280f304d8e"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc0c53579650a891f9b83fa3cecd4e00218e071d0ba00c4890f5be0c34887ed3"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:311710f9a2ee235f1403537b10c7687214bb1f2b9ebb52702c5aa4a77f0b3af7"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:408f8b0e2c04677e9c93f40eef3ab22f550fecb3011b187f66a096395ff3d9fd"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a4b4fb0dd4d2669070fb05b8b8824afd0af57587393015baee1cf9890242d9"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a943d297126c9230719c27fcbbeab57ecd5d15b0bd6bfd26e91bfcfe64220621"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a089e218654e740a41388893e090d2e2c22c29028c9d1353feb38638820bbeb"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win32.whl", hash = "sha256:fa561138a64f949f3e889eb9ab8c58e1504ab351d6cf55259dc4c248eaa19da6"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win_amd64.whl", hash = "sha256:7d74336c65705b986d12a7e337ba27ab2b9d819993851b140efdf029248e818e"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8c62fe2480dd61c532ccafdbce9b29dacc126fe8be0d9a927ca3e699b9491a"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2383146973a15435e4717f94c7509982770e3e54974c71f76500a0136f22810b"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8409de825f2c3b62ab15788635ccaec0c881c3f12a8af2b12ae4910a0a9aeef6"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0094c5dc698a5f78d3d1539853e8ecec02516b62b8223c970c86d44e7a80f6c7"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:edc16a50f5e1b7a06a2dcc1f2205b0b961074c123ed17ebda726f376a5ab0953"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f7703c2010355dd28f53deb644a05fc30f796bd8598b43f0ba678878780b6e4c"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win32.whl", hash = "sha256:1f9a727312ff6ad5248a4367358e2cf7e625e98b1028b1d7ab7b806b7d757513"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win_amd64.whl", hash = "sha256:a0ef36b28534f2a5771191be6edb44cc2673c7b2edf6deac6562400288664221"}, - {file = "SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a"}, - {file = "SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-win32.whl", hash = "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d"}, + {file = "SQLAlchemy-2.0.32-cp310-cp310-win_amd64.whl", hash = "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-win32.whl", hash = "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28"}, + {file = "SQLAlchemy-2.0.32-cp311-cp311-win_amd64.whl", hash = "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-win32.whl", hash = "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d"}, + {file = "SQLAlchemy-2.0.32-cp312-cp312-win_amd64.whl", hash = "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-win32.whl", hash = "sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da"}, + {file = "SQLAlchemy-2.0.32-cp37-cp37m-win_amd64.whl", hash = "sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-win32.whl", hash = "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65"}, + {file = "SQLAlchemy-2.0.32-cp38-cp38-win_amd64.whl", hash = "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-win32.whl", hash = "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78"}, + {file = "SQLAlchemy-2.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84"}, + {file = "SQLAlchemy-2.0.32-py3-none-any.whl", hash = "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"}, + {file = "SQLAlchemy-2.0.32.tar.gz", hash = "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -1671,13 +1838,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.5" +version = "0.13.2" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -1693,13 +1860,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.18" +version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] @@ -1709,13 +1876,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1754,13 +1921,13 @@ files = [ [[package]] name = "zipp" -version = "3.19.2" +version = "3.20.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, + {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, ] [package.extras] @@ -1770,4 +1937,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9.0" -content-hash = "6af5b5bc63b4c1a4be38e6b5f807694ca15273654e36382331a485cf6fbd40f1" +content-hash = "0218eea5bd0cd71cf433a4ff44a0bebbc4d86269483fe1149463f57094c4b160" diff --git a/pyproject.toml b/pyproject.toml index 35a335d..9b0e4db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ flask-cors = "^4.0.0" flask-restx = "^1.3.0" flask-api = "^3.1" flask-sqlalchemy = "^3.1.1" +cryptography = "^43.0.0" [tool.poetry.group.dev.dependencies] From 601b897fcc6b60c269afb818f9a227afb42f693c Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Mon, 19 Aug 2024 18:54:46 +0000 Subject: [PATCH 10/27] /version 4.1.0a5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9b0e4db..ba37abf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a4" +version = "4.1.0a5" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From f6eca443fa131f30e2c830062fa6416602fdeb9e Mon Sep 17 00:00:00 2001 From: bsatoriu <27687558+bsatoriu@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:52:12 -0700 Subject: [PATCH 11/27] Organizations & job queues management (#137) * Organizations endpoint * Add role name to members endpoint * Members creation_date formatting bug fix * Add role_id to member put endpoint * Org tree response formatting updates * Use root org as default id * Use root org as default id * Add creation_date to org response payload * Add membership array to Org post/get endpoints * Add maintainer prop to org output * Add parent_org_id prop to org output * Add membership updates to Org put endpoint * Org put endpoint bug fix * Org delete endpoint updates * Org delete endpoint bug fix * Org delete endpoint err msg * Job queue admin endpoints * Job queue admin get bug fix * Job queue admin delete bug fix * Filter algorithm resources by user/public queues * Refactor orgs and job queues * Queue refactoring & error handling --------- Co-authored-by: bsatoriu --- api/endpoints/admin.py | 97 ++++- api/endpoints/algorithm.py | 12 +- api/endpoints/members.py | 30 +- api/endpoints/organizations.py | 372 ++++++++++++++++++ api/maapapp.py | 2 + api/models/job_queue.py | 17 + api/models/organization.py | 25 ++ api/models/organization_job_queue.py | 14 + api/models/organization_membership.py | 24 ++ api/schemas/job_queue_schema.py | 11 + api/schemas/organization_job_queue_schema.py | 9 + api/schemas/organization_membership_schema.py | 9 + api/schemas/organization_schema.py | 9 + api/utils/job_queue.py | 166 ++++++++ api/utils/organization.py | 161 ++++++++ sql/materialized_views/org_tree.sql | 46 +++ 16 files changed, 992 insertions(+), 12 deletions(-) create mode 100644 api/endpoints/organizations.py create mode 100644 api/models/job_queue.py create mode 100644 api/models/organization.py create mode 100644 api/models/organization_job_queue.py create mode 100644 api/models/organization_membership.py create mode 100644 api/schemas/job_queue_schema.py create mode 100644 api/schemas/organization_job_queue_schema.py create mode 100644 api/schemas/organization_membership_schema.py create mode 100644 api/schemas/organization_schema.py create mode 100644 api/utils/job_queue.py create mode 100644 api/utils/organization.py create mode 100644 sql/materialized_views/org_tree.sql diff --git a/api/endpoints/admin.py b/api/endpoints/admin.py index 77afed6..8242a0b 100755 --- a/api/endpoints/admin.py +++ b/api/endpoints/admin.py @@ -2,20 +2,115 @@ from flask_restx import Resource from flask import request from flask_api import status +from api.models.job_queue import JobQueue +from api.models.organization import Organization +from api.models.organization_job_queue import OrganizationJobQueue from api.models.role import Role from api.restplus import api from api.auth.security import login_required from api.maap_database import db from api.models.pre_approved import PreApproved +from api.schemas.job_queue_schema import JobQueueSchema from api.schemas.pre_approved_schema import PreApprovedSchema from datetime import datetime import json - +from api.utils import job_queue from api.utils.http_util import err_response log = logging.getLogger(__name__) ns = api.namespace('admin', description='Operations related to the MAAP admin') +@ns.route('/job-queues') +class JobQueuesCls(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def get(self): + """ + Lists the job queues and associated organizations + :return: + """ + all_queues = job_queue.get_all_queues() + return all_queues + + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def post(self): + + """ + Create new job queue. + """ + + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + queue_name = req_data.get("queue_name", "") + if not isinstance(queue_name, str) or not queue_name: + return err_response("Valid queue name is required.") + + queue_description = req_data.get("queue_description", "") + if not isinstance(queue_description, str) or not queue_description: + return err_response("Valid queue description is required.") + + guest_tier = req_data.get("guest_tier", False) + orgs = req_data.get("orgs", []) + + new_queue = job_queue.create_queue(queue_name, queue_description, guest_tier, orgs) + return new_queue + + +@ns.route('/job-queues/') +class JobQueueCls(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def put(self, queue_id): + + """ + Update job queue. Only supplied fields are updated. + """ + + if not queue_id: + return err_response("Job queue id is required.") + + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + queue = db.session.query(JobQueue).filter_by(id=queue_id).first() + + if queue is None: + return err_response(msg="No job queue found with id " + queue_id) + + queue.queue_name = req_data.get("queue_name", queue.queue_name) + queue.queue_description = req_data.get("queue_description", queue.queue_description) + queue.guest_tier = req_data.get("guest_tier", queue.guest_tier) + orgs = req_data.get("orgs", []) + + updated_queue = job_queue.update_queue(queue, orgs) + return updated_queue + + + @api.doc(security='ApiKeyAuth') + @login_required(role=Role.ROLE_ADMIN) + def delete(self, queue_id): + """ + Delete job queue + """ + + queue = db.session.query(JobQueue).filter_by(id=queue_id).first() + queue_name = queue.queue_name + + if queue is None: + return err_response(msg="Job queue does not exist") + + job_queue.delete_queue(queue_id) + + return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(queue_name)} + + @ns.route('/pre-approved') class PreApprovedEmails(Resource): diff --git a/api/endpoints/algorithm.py b/api/endpoints/algorithm.py index 9ec7110..d4cabc3 100644 --- a/api/endpoints/algorithm.py +++ b/api/endpoints/algorithm.py @@ -1,5 +1,8 @@ import logging import os +from collections import namedtuple + +import sqlalchemy from flask import request, Response from flask_restx import Resource, reqparse from flask_api import status @@ -19,6 +22,8 @@ from datetime import datetime import json +from api.utils import job_queue + log = logging.getLogger(__name__) ns = api.namespace('mas', description='Operations to register an algorithm') @@ -450,6 +455,9 @@ def delete(self, algo_id): @ns.route('/algorithm/resource') class ResourceList(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() def get(self): """ This function would query DPS to see what resources (named based on memory space) are available for @@ -458,7 +466,9 @@ def get(self): """ try: response_body = {"code": None, "message": None} - queues = hysds.get_mozart_queues() + user = get_authorized_user() + queues = job_queue.get_user_queues(user.id) + response_body["code"] = status.HTTP_200_OK response_body["queues"] = queues response_body["message"] = "success" diff --git a/api/endpoints/members.py b/api/endpoints/members.py index b1c576c..ef7db94 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -4,6 +4,8 @@ from flask import request, jsonify, Response from flask_api import status from sqlalchemy.exc import SQLAlchemyError + +from api.models.role import Role from api.restplus import api import api.settings as settings from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request, \ @@ -42,18 +44,25 @@ class Member(Resource): @api.doc(security='ApiKeyAuth') @login_required() def get(self): - members = db.session.query( - Member_db.id, - Member_db.username, - Member_db.first_name, - Member_db.last_name, - Member_db.email, - Member_db.status, - Member_db.creation_date + + member_query = db.session.query( + Member_db, Role, + ).filter( + Member_db.role_id == Role.id ).order_by(Member_db.username).all() - member_schema = MemberSchema() - result = [json.loads(member_schema.dumps(m)) for m in members] + result = [{ + 'id': m.Member.id, + 'username': m.Member.username, + 'first_name': m.Member.first_name, + 'last_name': m.Member.last_name, + 'email': m.Member.email, + 'role_id': m.Member.role_id, + 'role_name': m.Role.role_name, + 'status': m.Member.status, + 'creation_date': m.Member.creation_date.strftime('%m/%d/%Y'), + } for m in member_query] + return result @@ -263,6 +272,7 @@ def put(self, key): member.public_ssh_key_modified_date = datetime.utcnow() member.public_ssh_key = req_data.get("public_ssh_key", member.public_ssh_key) member.public_ssh_key_name = req_data.get("public_ssh_key_name", member.public_ssh_key_name) + member.role_id = req_data.get("role_id", member.role_id) db.session.commit() member_schema = MemberSchema() diff --git a/api/endpoints/organizations.py b/api/endpoints/organizations.py new file mode 100644 index 0000000..9bf6e97 --- /dev/null +++ b/api/endpoints/organizations.py @@ -0,0 +1,372 @@ +import logging +import sqlalchemy +from flask_restx import Resource +from flask import request +from flask_api import status +from collections import namedtuple +from sqlalchemy.exc import SQLAlchemyError +from api.models.job_queue import JobQueue +from api.models.organization import Organization as Organization_db +from api.models.organization_job_queue import OrganizationJobQueue +from api.models.organization_membership import OrganizationMembership as OrganizationMembership_db +from api.models.member import Member +from api.models.role import Role +from api.restplus import api +from api.auth.security import login_required, get_authorized_user +from api.maap_database import db +from api.schemas.organization_job_queue_schema import OrganizationJobQueueSchema +from api.schemas.organization_membership_schema import OrganizationMembershipSchema +from api.schemas.organization_schema import OrganizationSchema +from datetime import datetime +import json + +from api.utils import organization +from api.utils.http_util import err_response + +log = logging.getLogger(__name__) +ns = api.namespace('organizations', description='Operations related to the MAAP organizations') + +@ns.route('') +class Organizations(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self): + """ + Lists the hierarchy of organizations using MAAP + :return: + """ + orgs = organization.get_organizations() + return orgs + + @api.doc(security='ApiKeyAuth') + @login_required() + def post(self): + """ + Create new organization + :return: + """ + + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + name = req_data.get("name", "") + if not isinstance(name, str) or not name: + return err_response("Valid org name is required.") + + root_org = db.session \ + .query(Organization_db) \ + .filter_by(parent_org_id=None) \ + .first() + + parent_org_id = req_data.get("parent_org_id", root_org.id) + if parent_org_id is None: + parent_org_id = root_org.id + + default_job_limit_count = req_data.get("default_job_limit_count", None) + default_job_limit_hours = req_data.get("default_job_limit_hours", None) + members = req_data.get("members", []) + + new_org = organization.create_organization(name, parent_org_id, default_job_limit_count, default_job_limit_hours, members) + + return new_org + + +@ns.route('/') +class Organization(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self, org_id): + """ + Retrieve organization + """ + org = organization.get_organization(org_id) + + if org is None: + return err_response(msg="No organization found with id " + org_id, code=status.HTTP_404_NOT_FOUND) + + org_schema = OrganizationSchema() + result = json.loads(org_schema.dumps(org)) + + return result + + @api.doc(security='ApiKeyAuth') + @login_required() + def put(self, org_id): + + """ + Update organization. Only supplied fields are updated. + """ + + if not org_id: + return err_response("Org id is required.") + + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + org = db.session.query(Organization_db).filter_by(id=org_id).first() + + if org is None: + return err_response(msg="No org found with id " + org_id) + + org.name = req_data.get("name", org.name) + org.parent_org_id = req_data.get("parent_org_id", org.parent_org_id) + org.default_job_limit_count = req_data.get("default_job_limit_count", org.default_job_limit_count) + org.default_job_limit_hours = req_data.get("default_job_limit_hours", org.default_job_limit_hours) + members = req_data.get("members", []) + + updated_org = organization.update_organization(org, members) + return updated_org + + + + @api.doc(security='ApiKeyAuth') + @login_required() + def delete(self, org_id): + """ + Delete organization + """ + + org = organization.get_organization(org_id) + + if org is None: + return err_response(msg="Organization does not exist") + + org_name = org.name + organization.delete_organization(org.id) + + return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(org_name)} + + +@ns.route('//membership') +class OrganizationMemberships(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self, org_id): + """ + Retrieve organization members + """ + try: + org_members = db.session.query( + OrganizationMembership_db, Member, Organization_db, + ).filter( + OrganizationMembership_db.member_id == Member.id, + ).filter( + OrganizationMembership_db.org_id == Organization_db.id, + ).filter( + OrganizationMembership_db.org_id == org_id, + ).order_by(Member.username).all() + + result = [{ + 'org_id': om.organization.id + } for om in org_members] + + return result + except SQLAlchemyError as ex: + raise ex + + +@ns.route('//membership/') +class OrganizationMembership(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def post(self, org_id, username): + """ + Add organization member + :return: + """ + try: + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + member = get_authorized_user() + membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, + org_id=org_id).first() + + if member.role_id != Role.ROLE_ADMIN and not membership.org_maintainer: + return err_response("Must be an org maintainer to add members.", status.HTTP_403_FORBIDDEN) + + org_member = db.session.query(Member).filter_by(username=username).first() + + if org_member is None: + return err_response("Valid username is required.") + + membership_dup = db.session.query(OrganizationMembership_db).filter_by(member_id=org_member.id, + org_id=org_id).first() + + if membership_dup is not None: + return err_response("Member {} already exists in org {}".format(username, org_id)) + + job_limit_count = req_data.get("job_limit_count", None) + job_limit_hours = req_data.get("job_limit_hours", None) + org_maintainer = req_data.get("org_maintainer", False) + + new_org_membership = OrganizationMembership_db(org_id=org_id, member_id=org_member.id, + job_limit_count=job_limit_count, + job_limit_hours=job_limit_hours, + org_maintainer=org_maintainer, + creation_date=datetime.utcnow()) + + db.session.add(new_org_membership) + db.session.commit() + + org_schema = OrganizationMembershipSchema() + return json.loads(org_schema.dumps(new_org_membership)) + + except SQLAlchemyError as ex: + raise ex + + @api.doc(security='ApiKeyAuth') + @login_required() + def delete(self, org_id, username): + """ + Delete organization member + """ + try: + member = get_authorized_user() + membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, + org_id=org_id).first() + + if membership is None: + return err_response("Org id {} for user {} was not found.".format(org_id, member.username)) + + if not membership.org_maintainer and member.role_id != Role.ROLE_ADMIN: + return err_response("Must be an org maintainer to remove members.", status.HTTP_403_FORBIDDEN) + + member_to_delete = db.session.query(Member).filter_by(username=username).first() + + if member_to_delete is None: + return err_response("Member {} was not found.".format(username)) + + membership_to_delete = db.session.query(OrganizationMembership_db).filter_by(member_id=member_to_delete.id, + org_id=org_id).first() + + if membership_to_delete is None: + return err_response("Org id {} for user {} was not found.".format(org_id, member_to_delete.username)) + + db.session.query(OrganizationMembership_db).filter_by(member_id=member_to_delete.id, org_id=org_id).delete() + db.session.commit() + + return {"code": status.HTTP_200_OK, + "message": "Successfully removed {} from org {}.".format(member_to_delete.username, org_id)} + + except SQLAlchemyError as ex: + raise ex + + +@ns.route('//job_queues') +class OrganizationJobQueues(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def get(self, org_id): + """ + Retrieve organization members + """ + try: + org_queues = db.session.query( + OrganizationJobQueue, JobQueue, Organization_db, + ).filter( + OrganizationJobQueue.job_queue_id == JobQueue.id, + ).filter( + OrganizationJobQueue.org_id == Organization_db.id, + ).filter( + OrganizationJobQueue.org_id == org_id, + ).order_by(JobQueue.queue_name).all() + + result = [{ + 'org_id': om.organization.id + } for om in org_queues] + + return result + except SQLAlchemyError as ex: + raise ex + + +@ns.route('//job_queues/') +class OrganizationJobQueueCls(Resource): + + @api.doc(security='ApiKeyAuth') + @login_required() + def post(self, org_id, queue_name): + """ + Add organization member + :return: + """ + try: + req_data = request.get_json() + if not isinstance(req_data, dict): + return err_response("Valid JSON body object required.") + + member = get_authorized_user() + membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, + org_id=org_id).first() + + if member.role_id != Role.ROLE_ADMIN and not membership.org_maintainer: + return err_response("Must be an org maintainer to add queues.", status.HTTP_403_FORBIDDEN) + + org_queue = db.session.query(JobQueue).filter_by(queue_name=queue_name).first() + + if org_queue is None: + return err_response("Valid job queue is required.") + + org_queue_dup = db.session.query(OrganizationJobQueue).filter_by(job_queue_id=org_queue.id, + org_id=org_id).first() + + if org_queue_dup is not None: + return err_response("Job queue {} already exists in org {}".format(queue_name, org_id)) + + new_org_queue = OrganizationJobQueue(org_id=org_id, job_queue_id=org_queue.id, + creation_date=datetime.utcnow()) + + db.session.add(new_org_queue) + db.session.commit() + + org_schema = OrganizationJobQueueSchema() + return json.loads(org_schema.dumps(new_org_queue)) + + except SQLAlchemyError as ex: + raise ex + + @api.doc(security='ApiKeyAuth') + @login_required() + def delete(self, org_id, queue_name): + """ + Delete organization member + """ + try: + member = get_authorized_user() + membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, + org_id=org_id).first() + + if membership is None: + return err_response("Org id {} for user {} was not found.".format(org_id, member.username)) + + if not membership.org_maintainer and member.role_id != Role.ROLE_ADMIN: + return err_response("Must be an org maintainer to remove members.", status.HTTP_403_FORBIDDEN) + + queue_to_delete = db.session.query(JobQueue).filter_by(queue_name=queue_name).first() + + if queue_to_delete is None: + return err_response("Job queue {} was not found.".format(queue_name)) + + org_queue_to_delete = db.session.query(OrganizationJobQueue).filter_by(job_queue_id=queue_to_delete.id, + org_id=org_id).first() + + if org_queue_to_delete is None: + return err_response("Org id {} for job queue {} was not found.".format(org_id, queue_name)) + + db.session.query(OrganizationJobQueue).filter_by(job_queue_id=queue_to_delete.id, org_id=org_id).delete() + db.session.commit() + + return {"code": status.HTTP_200_OK, + "message": "Successfully removed {} from org {}.".format(queue_name, org_id)} + + except SQLAlchemyError as ex: + raise ex diff --git a/api/maapapp.py b/api/maapapp.py index fd4fcba..1025ad2 100755 --- a/api/maapapp.py +++ b/api/maapapp.py @@ -15,6 +15,7 @@ from api.endpoints.wms import ns as wms_namespace from api.endpoints.members import ns as members_namespace from api.endpoints.environment import ns as environment_namespace +from api.endpoints.organizations import ns as organizations_namespace from api.endpoints.admin import ns as admin_namespace from api.restplus import api from api.maap_database import db @@ -103,6 +104,7 @@ def initialize_app(flask_app): api.add_namespace(wms_namespace) api.add_namespace(members_namespace) api.add_namespace(environment_namespace) + api.add_namespace(organizations_namespace) api.add_namespace(admin_namespace) flask_app.register_blueprint(blueprint) diff --git a/api/models/job_queue.py b/api/models/job_queue.py new file mode 100644 index 0000000..0b537a3 --- /dev/null +++ b/api/models/job_queue.py @@ -0,0 +1,17 @@ +from api.models import Base +from api.maap_database import db + +class JobQueue(Base): + __tablename__ = 'job_queue' + + id = db.Column(db.Integer, primary_key=True) + queue_name = db.Column(db.String()) + queue_description = db.Column(db.String()) + # Whether the queue is available to public 'Guest' users + guest_tier = db.Column(db.Boolean()) + creation_date = db.Column(db.DateTime()) + + def __repr__(self): + return "".format(self=self) + + diff --git a/api/models/organization.py b/api/models/organization.py new file mode 100644 index 0000000..7aed319 --- /dev/null +++ b/api/models/organization.py @@ -0,0 +1,25 @@ +from api.models import Base +from api.maap_database import db + +class Organization(Base): + __tablename__ = 'organization' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String()) + parent_org_id = db.Column(db.Integer, db.ForeignKey('organization.id')) + + # The maximum number of jobs that org members can run per the defined hour(s). + # Used in conjunction with default_job_limit_hours. + # A value of null or zero equates to unlimited jobs. + default_job_limit_count = db.Column(db.Integer) + + # The number of hours during which an org member can run their allotment of jobs. + # Used in conjunction with default_job_limit_count. + # A value of null or zero equates to unlimited hours. + default_job_limit_hours = db.Column(db.Integer) + creation_date = db.Column(db.DateTime()) + + def __repr__(self): + return "".format(self=self) + + diff --git a/api/models/organization_job_queue.py b/api/models/organization_job_queue.py new file mode 100644 index 0000000..bb9ca39 --- /dev/null +++ b/api/models/organization_job_queue.py @@ -0,0 +1,14 @@ +from api.models import Base +from api.maap_database import db + + +class OrganizationJobQueue(Base): + __tablename__ = 'organization_job_queue' + + id = db.Column(db.Integer, primary_key=True) + job_queue_id = db.Column(db.Integer, db.ForeignKey('job_queue.id'), nullable=False) + org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) + creation_date = db.Column(db.DateTime()) + + def __repr__(self): + return "".format(self=self) \ No newline at end of file diff --git a/api/models/organization_membership.py b/api/models/organization_membership.py new file mode 100644 index 0000000..64f77e4 --- /dev/null +++ b/api/models/organization_membership.py @@ -0,0 +1,24 @@ +from api.models import Base +from api.maap_database import db + + +class OrganizationMembership(Base): + __tablename__ = 'organization_membership' + + id = db.Column(db.Integer, primary_key=True) + member_id = db.Column(db.Integer, db.ForeignKey('member.id'), nullable=False) + org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) + org_maintainer = db.Column(db.Boolean()) + # The maximum number of jobs that this org member can run per the defined hour(s). + # Used in conjunction with job_limit_hours. + # A value of null or zero equates to unlimited jobs. + job_limit_count = db.Column(db.Integer) + + # The number of hours during which this org member can run their allotment of jobs. + # Used in conjunction with job_limit_count. + # A value of null or zero equates to unlimited hours. + job_limit_hours = db.Column(db.Integer) + creation_date = db.Column(db.DateTime()) + + def __repr__(self): + return "".format(self=self) \ No newline at end of file diff --git a/api/schemas/job_queue_schema.py b/api/schemas/job_queue_schema.py new file mode 100644 index 0000000..d451d92 --- /dev/null +++ b/api/schemas/job_queue_schema.py @@ -0,0 +1,11 @@ +from api.models.job_queue import JobQueue +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class JobQueueSchema(SQLAlchemyAutoSchema): + class Meta: + model = JobQueue + include_relationships = True + load_instance = True + + diff --git a/api/schemas/organization_job_queue_schema.py b/api/schemas/organization_job_queue_schema.py new file mode 100644 index 0000000..5464465 --- /dev/null +++ b/api/schemas/organization_job_queue_schema.py @@ -0,0 +1,9 @@ +from api.models.organization_job_queue import OrganizationJobQueue +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class OrganizationJobQueueSchema(SQLAlchemyAutoSchema): + class Meta: + model = OrganizationJobQueue + include_fk = True + load_instance = True diff --git a/api/schemas/organization_membership_schema.py b/api/schemas/organization_membership_schema.py new file mode 100644 index 0000000..628847a --- /dev/null +++ b/api/schemas/organization_membership_schema.py @@ -0,0 +1,9 @@ +from api.models.organization_membership import OrganizationMembership +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class OrganizationMembershipSchema(SQLAlchemyAutoSchema): + class Meta: + model = OrganizationMembership + include_fk = True + load_instance = True diff --git a/api/schemas/organization_schema.py b/api/schemas/organization_schema.py new file mode 100644 index 0000000..ba71b75 --- /dev/null +++ b/api/schemas/organization_schema.py @@ -0,0 +1,9 @@ +from api.models.organization import Organization +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + + +class OrganizationSchema(SQLAlchemyAutoSchema): + class Meta: + model = Organization + include_fk = True + load_instance = True diff --git a/api/utils/job_queue.py b/api/utils/job_queue.py new file mode 100644 index 0000000..5b25368 --- /dev/null +++ b/api/utils/job_queue.py @@ -0,0 +1,166 @@ +import json +import logging +from collections import namedtuple +from datetime import datetime + +import sqlalchemy +from sqlalchemy.exc import SQLAlchemyError +from api.maap_database import db +from api.models.job_queue import JobQueue +from api.models.organization import Organization +from api.models.organization_job_queue import OrganizationJobQueue +import api.utils.hysds_util as hysds +from api.schemas.job_queue_schema import JobQueueSchema + +log = logging.getLogger(__name__) + + +def get_user_queues(user_id): + + try: + user_queues = [] + query = """select jq.queue_name from organization_membership m + inner join public.organization_job_queue ojq on m.org_id = ojq.org_id + inner join public.job_queue jq on jq.id = ojq.job_queue_id + where m.member_id = {} + union + select queue_name + from job_queue + where guest_tier = true""".format(user_id) + queue_list = db.session.execute(sqlalchemy.text(query)) + + Record = namedtuple('Record', queue_list.keys()) + queue_records = [Record(*r) for r in queue_list.fetchall()] + + for r in queue_records: + user_queues.append(r.queue_name) + + return user_queues + + except SQLAlchemyError as ex: + raise ex + +def get_all_queues(): + try: + result = [] + + queues = db.session.query( + JobQueue.id, + JobQueue.queue_name, + JobQueue.queue_description, + JobQueue.guest_tier, + JobQueue.creation_date + ).order_by(JobQueue.queue_name).all() + + orgs_query = db.session.query( + Organization, OrganizationJobQueue, + ).filter( + Organization.id == OrganizationJobQueue.org_id + ).order_by(Organization.name).all() + + hysds_queues = hysds.get_mozart_queues() + + for q in queues: + queue = { + 'id': q.id, + 'queue_name': q.queue_name, + 'queue_description': q.queue_description, + 'guest_tier': q.guest_tier, + 'status': 'Online' if q.queue_name in hysds_queues else 'Offline', + 'orgs': [], + 'creation_date': q.creation_date.strftime('%m/%d/%Y'), + } + + for o in orgs_query: + if o.OrganizationJobQueue.job_queue_id == q.id: + queue['orgs'].append({ + 'id': o.Organization.id, + 'org_name': o.Organization.name, + 'default_job_limit_count': o.Organization.default_job_limit_count, + 'default_job_limit_hours': o.Organization.default_job_limit_hours + }) + + result.append(queue) + + unassigned_queues = (hq for hq in hysds_queues if hq not in map(_queue_name, queues)) + for uq in unassigned_queues: + result.append({ + 'id': 0, + 'queue_name': uq, + 'queue_description': '', + 'guest_tier': False, + 'status': 'Unassigned', + 'orgs': [], + 'creation_date': None, + }) + + return result + except SQLAlchemyError as ex: + raise ex + +def _queue_name(q): + return q.queue_name + +def create_queue(queue_name, queue_description, guest_tier, orgs): + + try: + new_queue = JobQueue(queue_name=queue_name, queue_description=queue_description, guest_tier=guest_tier, + creation_date=datetime.utcnow()) + + db.session.add(new_queue) + db.session.commit() + + queue_orgs = [] + for queue_org in orgs: + queue_orgs.append(OrganizationJobQueue(org_id=queue_org['org_id'], job_queue_id=new_queue.id, + creation_date=datetime.utcnow())) + + if len(queue_orgs) > 0: + db.session.add_all(queue_orgs) + db.session.commit() + + org_schema = JobQueueSchema() + return json.loads(org_schema.dumps(new_queue)) + + except SQLAlchemyError as ex: + raise ex + +def update_queue(queue, orgs): + try: + # Update queue + db.session.commit() + + # Update org assignments + db.session.execute( + db.delete(OrganizationJobQueue).filter_by(job_queue_id=queue.id) + ) + db.session.commit() + + queue_orgs = [] + for queue_org in orgs: + queue_orgs.append( + OrganizationJobQueue(org_id=queue_org['org_id'], job_queue_id=queue.id, + creation_date=datetime.utcnow())) + + if len(queue_orgs) > 0: + db.session.add_all(queue_orgs) + db.session.commit() + + queue_schema = JobQueueSchema() + return json.loads(queue_schema.dumps(queue)) + + except SQLAlchemyError as ex: + raise ex + +def delete_queue(queue_id): + try: + # Clear orgs + db.session.execute( + db.delete(OrganizationJobQueue).filter_by(job_queue_id=queue_id) + ) + db.session.commit() + + db.session.query(JobQueue).filter_by(id=queue_id).delete() + db.session.commit() + except SQLAlchemyError as ex: + raise ex diff --git a/api/utils/organization.py b/api/utils/organization.py new file mode 100644 index 0000000..2d1b382 --- /dev/null +++ b/api/utils/organization.py @@ -0,0 +1,161 @@ +import logging +from collections import namedtuple +from datetime import datetime +import json +import sqlalchemy +from sqlalchemy.exc import SQLAlchemyError +from api.maap_database import db +from api.models.job_queue import JobQueue +from api.models.member import Member +from api.models.organization import Organization +from api.models.organization_job_queue import OrganizationJobQueue +from api.models.organization_membership import OrganizationMembership +from api.schemas.organization_schema import OrganizationSchema + +log = logging.getLogger(__name__) + + +def get_organizations(): + try: + result = [] + otree = db.session.execute(sqlalchemy.text('select * from org_tree order by row_number')) + + queues_query = db.session.query( + JobQueue, OrganizationJobQueue, + ).filter( + JobQueue.id == OrganizationJobQueue.job_queue_id + ).order_by(JobQueue.queue_name).all() + + membership_query = db.session.query( + Member, OrganizationMembership, + ).filter( + Member.id == OrganizationMembership.member_id + ).order_by(Member.first_name).all() + + Record = namedtuple('Record', otree.keys()) + org_tree_records = [Record(*r) for r in otree.fetchall()] + for r in org_tree_records: + org = { + 'id': r.id, + 'parent_org_id': r.parent_org_id, + 'name': r.name, + 'depth': r.depth, + 'member_count': r.member_count, + 'default_job_limit_count': r.default_job_limit_count, + 'default_job_limit_hours': r.default_job_limit_hours, + 'job_queues': [], + 'members': [], + 'creation_date': r.creation_date.strftime('%m/%d/%Y'), + } + + for q in queues_query: + if q.OrganizationJobQueue.org_id == r.id: + org['job_queues'].append({ + 'id': q.JobQueue.id, + 'queue_name': q.JobQueue.queue_name, + 'queue_description': q.JobQueue.queue_description + }) + + for m in membership_query: + if m.OrganizationMembership.org_id == r.id: + org['members'].append({ + 'id': m.Member.id, + 'first_name': m.Member.first_name, + 'last_name': m.Member.last_name, + 'username': m.Member.username, + 'email': m.Member.email, + 'maintainer': m.OrganizationMembership.org_maintainer + }) + + result.append(org) + + return result + except SQLAlchemyError as ex: + raise ex + +def get_organization(org_id): + try: + org = db.session \ + .query(Organization) \ + .filter_by(id=org_id) \ + .first() + return org + + except SQLAlchemyError as ex: + raise ex + +def create_organization(name, parent_org_id, default_job_limit_count, default_job_limit_hours, members): + + try: + new_org = Organization(name=name, parent_org_id=parent_org_id, default_job_limit_count=default_job_limit_count, + default_job_limit_hours=default_job_limit_hours, creation_date=datetime.utcnow()) + + db.session.add(new_org) + db.session.commit() + + org_members = [] + for org_member in members: + org_members.append(OrganizationMembership(member_id=org_member['member_id'], org_id=new_org.id, + org_maintainer=org_member['maintainer'], + creation_date=datetime.utcnow())) + + if len(org_members) > 0: + db.session.add_all(org_members) + db.session.commit() + + org_schema = OrganizationSchema() + return json.loads(org_schema.dumps(new_org)) + + except SQLAlchemyError as ex: + raise ex + +def update_organization(org, members): + + try: + # Update org + db.session.commit() + + # Update membership + db.session.execute( + db.delete(OrganizationMembership).filter_by(org_id=org.id) + ) + db.session.commit() + + org_members = [] + for org_member in members: + org_members.append(OrganizationMembership( + member_id=org_member['member_id'], + org_id=org.id, + org_maintainer=org_member['maintainer'], + creation_date=datetime.utcnow())) + + if len(org_members) > 0: + db.session.add_all(org_members) + db.session.commit() + + org_schema = OrganizationSchema() + return json.loads(org_schema.dumps(org)) + + except SQLAlchemyError as ex: + raise ex + +def delete_organization(org_id): + try: + + # Clear membership + db.session.execute( + db.delete(OrganizationMembership).filter_by(org_id=org_id) + ) + db.session.commit() + + # Clear job queues + db.session.execute( + db.delete(OrganizationJobQueue).filter_by(org_id=org_id) + ) + db.session.commit() + + db.session.query(Organization).filter_by(id=org_id).delete() + db.session.commit() + + except SQLAlchemyError as ex: + raise ex diff --git a/sql/materialized_views/org_tree.sql b/sql/materialized_views/org_tree.sql new file mode 100644 index 0000000..a90019a --- /dev/null +++ b/sql/materialized_views/org_tree.sql @@ -0,0 +1,46 @@ +create view org_tree + (row_number, member_count, depth, id, name, parent_org_id, default_job_limit_count, default_job_limit_hours, + creation_date) +as +WITH RECURSIVE + node_rec AS (SELECT 1 AS row_count, + 0 AS member_count, + 1 AS depth, + organization.id, + organization.name, + organization.parent_org_id, + organization.default_job_limit_count, + organization.default_job_limit_hours, + organization.creation_date + FROM organization + WHERE organization.parent_org_id IS NULL + UNION ALL + SELECT 1 AS row_count, + 0 AS member_count, + r.depth + 1, + n.id, + n.name, + n.parent_org_id, + n.default_job_limit_count, + n.default_job_limit_hours, + n.creation_date + FROM node_rec r + JOIN organization n ON n.parent_org_id = r.id) SEARCH DEPTH FIRST BY name, + id SET path + SELECT row_number() OVER (ORDER BY node_rec.path, node_rec.name) AS row_number, + ( SELECT count(*) AS count + FROM organization_membership + WHERE organization_membership.org_id = node_rec.id) AS member_count, + node_rec.depth, + node_rec.id, + node_rec.name, + node_rec.parent_org_id, + node_rec.default_job_limit_count, + node_rec.default_job_limit_hours, + node_rec.creation_date + FROM node_rec + WHERE node_rec.parent_org_id IS NOT NULL + ORDER BY node_rec.path, node_rec.name; + +alter table org_tree + owner to postgres; \ No newline at end of file From 781e7a63c563296af2faae3f265cd4139087ec85 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Mon, 9 Sep 2024 22:52:53 +0000 Subject: [PATCH 12/27] /version 4.1.0a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba37abf..9ebd4db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a5" +version = "4.1.0a6" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From ef8ee4289cd8c787e73aab60b9364f5ab5c50df2 Mon Sep 17 00:00:00 2001 From: bsatoriu <27687558+bsatoriu@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:21:39 -0700 Subject: [PATCH 13/27] Update CHANGELOG.md --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2247c86..534e81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v4.1.0] - 2024-09-10 +- [pull/131](https://github.com/MAAP-Project/maap-api-nasa/pull/131) - Added query params to job list endpoint +- [pull/135](https://github.com/MAAP-Project/maap-api-nasa/pull/135) - User secret management +- [pull/137](https://github.com/MAAP-Project/maap-api-nasa/pull/137) - Organizations & job queues management +- [pull/136](https://github.com/MAAP-Project/maap-api-nasa/pull/136) - Add support for DPS sandbox queue +- [pull/132](https://github.com/MAAP-Project/maap-api-nasa/pull/132) - Remove {username} param from DPS job list endpoint + ## [v4.0.0] - 2024-06-26 - [issues/111](https://github.com/MAAP-Project/maap-api-nasa/issues/111) - Implement github actions CICD and convert to poetry based build - [pull/110](https://github.com/MAAP-Project/maap-api-nasa/pull/110) - Remove postgres from docker-compose @@ -16,4 +23,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [unreleased]: https://github.com/MAAP-Project/maap-api-nasa/v4.0.0...HEAD -[v4.0.0]: https://github.com/MAAP-Project/maap-api-nasa/compare/v3.1.5...v4.0.0 \ No newline at end of file +[v4.1.0]: https://github.com/MAAP-Project/maap-api-nasa/compare/v4.0.0...v4.1.0 +[v4.0.0]: https://github.com/MAAP-Project/maap-api-nasa/compare/v3.1.5...v4.0.0 From f510858cd1435969c148068af6847c71ce864a9e Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Tue, 10 Sep 2024 21:22:17 +0000 Subject: [PATCH 14/27] /version 4.1.0a7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ebd4db..2fe17e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a6" +version = "4.1.0a7" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From cfaa377d7f544688ca8b859d8a6d843c60858efe Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Tue, 10 Sep 2024 21:24:47 +0000 Subject: [PATCH 15/27] /version 4.2.0a0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2fe17e7..05dbc45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0a7" +version = "4.2.0a0" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From df85c6b082b29a094939e81415ad8acdfd20ea3b Mon Sep 17 00:00:00 2001 From: Sujen Shah Date: Thu, 12 Sep 2024 10:34:47 -0700 Subject: [PATCH 16/27] Add support for DPS sandbox queue (#136) * Respond with HTTP 400 if invalid queue provided during registration * Update dos job time limits if using dps sand box queue * Ensure that message is of type string * update code to use new job_queue.get_user_queues() function * Move queue settings to db * add is_default and time_limit_minutes to job_queue endpoints * Job submission bug fix --------- Co-authored-by: bsatoriu --- api/endpoints/admin.py | 9 ++-- api/endpoints/algorithm.py | 25 +++++++--- api/endpoints/job.py | 12 ++++- api/models/job_queue.py | 4 ++ api/settings.py | 3 +- api/utils/hysds_util.py | 28 ++++++++---- api/utils/job_queue.py | 76 ++++++++++++++++++++++++++++--- api/utils/ogc_translate.py | 2 +- test/api/utils/test_hysds_util.py | 66 +++++++++++++++++++++++++++ 9 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 test/api/utils/test_hysds_util.py diff --git a/api/endpoints/admin.py b/api/endpoints/admin.py index 8242a0b..2ed587d 100755 --- a/api/endpoints/admin.py +++ b/api/endpoints/admin.py @@ -3,14 +3,11 @@ from flask import request from flask_api import status from api.models.job_queue import JobQueue -from api.models.organization import Organization -from api.models.organization_job_queue import OrganizationJobQueue from api.models.role import Role from api.restplus import api from api.auth.security import login_required from api.maap_database import db from api.models.pre_approved import PreApproved -from api.schemas.job_queue_schema import JobQueueSchema from api.schemas.pre_approved_schema import PreApprovedSchema from datetime import datetime import json @@ -55,9 +52,11 @@ def post(self): return err_response("Valid queue description is required.") guest_tier = req_data.get("guest_tier", False) + is_default = req_data.get("is_default", False) + time_limit_minutes = req_data.get("time_limit_minutes", 0) orgs = req_data.get("orgs", []) - new_queue = job_queue.create_queue(queue_name, queue_description, guest_tier, orgs) + new_queue = job_queue.create_queue(queue_name, queue_description, guest_tier, is_default, time_limit_minutes, orgs) return new_queue @@ -87,6 +86,8 @@ def put(self, queue_id): queue.queue_name = req_data.get("queue_name", queue.queue_name) queue.queue_description = req_data.get("queue_description", queue.queue_description) queue.guest_tier = req_data.get("guest_tier", queue.guest_tier) + queue.is_default = req_data.get("is_default", queue.is_default) + queue.time_limit_minutes = req_data.get("time_limit_minutes", queue.time_limit_minutes) orgs = req_data.get("orgs", []) updated_queue = job_queue.update_queue(queue, orgs) diff --git a/api/endpoints/algorithm.py b/api/endpoints/algorithm.py index d4cabc3..bd573c9 100644 --- a/api/endpoints/algorithm.py +++ b/api/endpoints/algorithm.py @@ -13,6 +13,7 @@ import traceback import api.utils.github_util as git import api.utils.hysds_util as hysds +import api.utils.http_util as http_util import api.settings as settings import api.utils.ogc_translate as ogc from api.auth.security import get_authorized_user, login_required @@ -260,12 +261,16 @@ def post(self): try: # validate if input queue is valid - if resource is None: resource = settings.DEFAULT_QUEUE - if resource not in hysds.get_mozart_queues(): - response_body["code"] = status.HTTP_500_INTERNAL_SERVER_ERROR - response_body["message"] = "The resource {} is invalid. Please select from one of {}".format(resource, hysds.get_mozart_queues()) - response_body["error"] = "Invalid queue in request: {}".format(req_data) - return response_body, 500 + user = get_authorized_user() + if resource is None: + resource = job_queue.get_default_queue().queue_name + else: + valid_queues = job_queue.get_user_queues(user.id) + valid_queue_names = list(map(lambda q: q.queue_name, valid_queues)) + if resource not in valid_queue_names: + return http_util.err_response(msg=f"User does not have permissions for resource {resource}." + f"Please select from one of {valid_queue_names}", + code=status.HTTP_400_BAD_REQUEST) # clean up any old specs from the repo repo = git.clean_up_git_repo(repo, repo_name=settings.REPO_NAME) # creating hysds-io file @@ -316,6 +321,7 @@ def post(self): hysds.write_file("{}/{}".format(settings.REPO_PATH, settings.REPO_NAME), "job-submission.json", job_submission_json) logging.debug("Created spec files") + except Exception as ex: tb = traceback.format_exc() response_body["code"] = status.HTTP_500_INTERNAL_SERVER_ERROR @@ -416,6 +422,10 @@ def get(self, algo_id): try: job_type = "job-{}".format(algo_id) response = hysds.get_job_spec(job_type) + if response is None: + return Response(ogc.get_exception(type="FailedSearch", origin_process="DescribeProcess", + ex_message="Algorithm not found. {}".format(job_type)), + status=status.HTTP_404_NOT_FOUND,mimetype='text/xml') params = response.get("result").get("params") queue = response.get("result").get("recommended-queues")[0] response_body = ogc.describe_process_response(algo_id, params, queue) @@ -468,9 +478,10 @@ def get(self): response_body = {"code": None, "message": None} user = get_authorized_user() queues = job_queue.get_user_queues(user.id) + queue_names = list(map(lambda q: q.queue_name, queues)) response_body["code"] = status.HTTP_200_OK - response_body["queues"] = queues + response_body["queues"] = queue_names response_body["message"] = "success" return response_body except Exception as ex: diff --git a/api/endpoints/job.py b/api/endpoints/job.py index 0e7474b..3adb2d7 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -5,6 +5,7 @@ from api.restplus import api import api.utils.hysds_util as hysds import api.utils.ogc_translate as ogc +import api.utils.job_queue as job_queue import api.settings as settings try: import urllib.parse as urlparse @@ -60,8 +61,11 @@ def post(self): try: dedup = "false" if dedup is None else dedup - queue = hysds.get_recommended_queue(job_type=job_type) if queue is None or queue is "" else queue - response = hysds.mozart_submit_job(job_type=job_type, params=params, dedup=dedup, queue=queue, + user = get_authorized_user() + queue = job_queue.validate_or_get_queue(queue, job_type, user.id) + if job_queue.contains_time_limit(queue): + hysds.set_timelimit_for_dps_sandbox(params, queue) + response = hysds.mozart_submit_job(job_type=job_type, params=params, dedup=dedup, queue=queue.queue_name, identifier=identifier) logging.info("Mozart Response: {}".format(json.dumps(response))) @@ -78,6 +82,10 @@ def post(self): return Response(ogc.status_response(job_id=job_id, job_status=job_status), mimetype='text/xml') else: raise Exception(response.get("message")) + except ValueError as ex: + logging.error(traceback.format_exc()) + return Response(ogc.get_exception(type="FailedJobSubmit", origin_process="Execute", + ex_message=str(ex)), status.HTTP_400_BAD_REQUEST) except Exception as ex: logging.info("Error submitting job: {}".format(ex)) return Response(ogc.get_exception(type="FailedJobSubmit", origin_process="Execute", diff --git a/api/models/job_queue.py b/api/models/job_queue.py index 0b537a3..e2bb4a2 100644 --- a/api/models/job_queue.py +++ b/api/models/job_queue.py @@ -9,6 +9,10 @@ class JobQueue(Base): queue_description = db.Column(db.String()) # Whether the queue is available to public 'Guest' users guest_tier = db.Column(db.Boolean()) + # Whether the queue is used as a default when no queues are specified + is_default = db.Column(db.Boolean()) + # The maximum time, in minutes, that jobs are allowed to run using this queue + time_limit_minutes = db.Column(db.Integer) creation_date = db.Column(db.DateTime()) def __repr__(self): diff --git a/api/settings.py b/api/settings.py index c36e872..94dae86 100755 --- a/api/settings.py +++ b/api/settings.py @@ -5,7 +5,6 @@ def str2bool(v): MAAP_API_URL = os.getenv('MAAP_API_URL', "http://localhost:5000/api") -PROJECT_QUEUE_PREFIX = os.getenv('PROJECT_QUEUE_PREFIX', "maap") API_HOST_URL = os.getenv('API_HOST_URL', 'http://0.0.0.0:5000/') # Flask settings @@ -59,12 +58,12 @@ def str2bool(v): MOZART_URL = os.getenv('MOZART_URL', 'https://[MOZART_IP]/mozart/api/v0.2') MOZART_V1_URL = os.getenv('MOZART_V1_URL', 'https://[MOZART_IP]/mozart/api/v0.1') # new from sister GRQ_URL = os.getenv('GRQ_URL', 'http://[GRQ_IP]:8878/api/v0.1') # new from sister -DEFAULT_QUEUE = os.getenv('DEFAULT_QUEUE', 'test-job_worker-large') LW_QUEUE = os.getenv('LW_QUEUE', 'system-jobs-queue') HYSDS_LW_VERSION = os.getenv('HYSDS_LW_VERSION', 'v1.2.2') GRQ_REST_URL = os.getenv('GRQ_REST_URL', 'http://[GRQ_IP]/api/v0.1') S3_CODE_BUCKET = os.getenv('S3_CODE_BUCKET', 's3://[S3_BUCKET_NAME]') DPS_MACHINE_TOKEN = os.getenv('DPS_MACHINE_TOKEN', '') +PROJECT_QUEUE_PREFIX = os.getenv('PROJECT_QUEUE_PREFIX', "maap") # FASTBROWSE API TILER_ENDPOINT = os.getenv('TILER_ENDPOINT', 'https://d852m4cmf5.execute-api.us-east-1.amazonaws.com') diff --git a/api/utils/hysds_util.py b/api/utils/hysds_util.py index 97a8a71..9659bdb 100644 --- a/api/utils/hysds_util.py +++ b/api/utils/hysds_util.py @@ -7,6 +7,9 @@ import time import copy +import api.utils.job_queue +from api.models import job_queue + log = logging.getLogger(__name__) STATUS_JOB_STARTED = "job-started" @@ -202,7 +205,7 @@ def create_hysds_io(algorithm_description, inputs, verified=False, submission_ty hysds_io["params"] = params return hysds_io -def create_job_spec(run_command, inputs, disk_usage, queue_name=settings.DEFAULT_QUEUE, verified=False): +def create_job_spec(run_command, inputs, disk_usage, queue_name, verified=False): """ Creates the contents of the job spec file :param run_command: @@ -410,7 +413,7 @@ def get_algorithms(): return maap_algo_list -def mozart_submit_job(job_type, params={}, queue=settings.DEFAULT_QUEUE, dedup="false", identifier="maap-api_submit"): +def mozart_submit_job(job_type, params={}, queue="", dedup="false", identifier="maap-api_submit"): """ Submit a job to Mozart :param job_type: @@ -549,7 +552,7 @@ def get_recommended_queue(job_type): response = get_job_spec(job_type) recommended_queues = response.get("result", None).get("recommended-queues", None) recommended_queue = recommended_queues[0] if type(recommended_queues) is list else None - return recommended_queue if recommended_queue != "" else settings.DEFAULT_QUEUE + return recommended_queue if recommended_queue != "" else api.utils.job_queue.get_default_queue().queue_name def validate_job_submit(hysds_io, user_params): @@ -588,8 +591,8 @@ def validate_job_submit(hysds_io, user_params): if known_params.get(p).get("default") is not None: validated_params[p] = known_params.get(p).get("default") else: - raise Exception("Parameter {} missing from inputs. Didn't find any default set for it in " - "algorithm specification. Please specify it and attempt to submit.".format(p)) + raise ValueError("Parameter {} missing from inputs. Didn't find any default set for it in " + "algorithm specification. Please specify it and attempt to submit.".format(p)) return validated_params @@ -776,8 +779,13 @@ def revoke_mozart_job(job_id, wait_for_completion=False): return poll_for_completion(lw_job_id) -def pele_get_product_by_id(id): - return - - - +def set_timelimit_for_dps_sandbox(params: dict, queue: job_queue): + """ + Sets the soft_time_limit and time_limit parameters for DPS sandbox queue + at job submission + :param params: + :param queue: Job queue + :return: params + """ + params.update({"soft_time_limit": queue.time_limit_minutes * 60, + "time_limit": queue.time_limit_minutes * 60}) diff --git a/api/utils/job_queue.py b/api/utils/job_queue.py index 5b25368..aa50e08 100644 --- a/api/utils/job_queue.py +++ b/api/utils/job_queue.py @@ -6,25 +6,26 @@ import sqlalchemy from sqlalchemy.exc import SQLAlchemyError from api.maap_database import db +from api.models import job_queue from api.models.job_queue import JobQueue from api.models.organization import Organization from api.models.organization_job_queue import OrganizationJobQueue import api.utils.hysds_util as hysds from api.schemas.job_queue_schema import JobQueueSchema +from api import settings log = logging.getLogger(__name__) def get_user_queues(user_id): - try: user_queues = [] - query = """select jq.queue_name from organization_membership m + query = """select jq.id, jq.queue_name, jq.is_default, jq.time_limit_minutes from organization_membership m inner join public.organization_job_queue ojq on m.org_id = ojq.org_id inner join public.job_queue jq on jq.id = ojq.job_queue_id where m.member_id = {} union - select queue_name + select id, queue_name, is_default, time_limit_minutes from job_queue where guest_tier = true""".format(user_id) queue_list = db.session.execute(sqlalchemy.text(query)) @@ -33,13 +34,14 @@ def get_user_queues(user_id): queue_records = [Record(*r) for r in queue_list.fetchall()] for r in queue_records: - user_queues.append(r.queue_name) + user_queues.append(JobQueue(id=r.id, queue_name=r.queue_name, is_default=r.is_default, time_limit_minutes=r.time_limit_minutes)) return user_queues except SQLAlchemyError as ex: raise ex + def get_all_queues(): try: result = [] @@ -49,6 +51,8 @@ def get_all_queues(): JobQueue.queue_name, JobQueue.queue_description, JobQueue.guest_tier, + JobQueue.is_default, + JobQueue.time_limit_minutes, JobQueue.creation_date ).order_by(JobQueue.queue_name).all() @@ -66,6 +70,8 @@ def get_all_queues(): 'queue_name': q.queue_name, 'queue_description': q.queue_description, 'guest_tier': q.guest_tier, + 'is_default': q.is_default, + 'time_limit_minutes': q.time_limit_minutes, 'status': 'Online' if q.queue_name in hysds_queues else 'Offline', 'orgs': [], 'creation_date': q.creation_date.strftime('%m/%d/%Y'), @@ -89,6 +95,8 @@ def get_all_queues(): 'queue_name': uq, 'queue_description': '', 'guest_tier': False, + 'is_default': False, + 'time_limit_minutes': 0, 'status': 'Unassigned', 'orgs': [], 'creation_date': None, @@ -98,18 +106,22 @@ def get_all_queues(): except SQLAlchemyError as ex: raise ex + def _queue_name(q): return q.queue_name -def create_queue(queue_name, queue_description, guest_tier, orgs): +def create_queue(queue_name, queue_description, guest_tier, is_default, time_limit_minutes, orgs): try: new_queue = JobQueue(queue_name=queue_name, queue_description=queue_description, guest_tier=guest_tier, - creation_date=datetime.utcnow()) + is_default=is_default, time_limit_minutes=time_limit_minutes, creation_date=datetime.utcnow()) db.session.add(new_queue) db.session.commit() + if is_default: + _reset_queue_default(new_queue.id) + queue_orgs = [] for queue_org in orgs: queue_orgs.append(OrganizationJobQueue(org_id=queue_org['org_id'], job_queue_id=new_queue.id, @@ -125,11 +137,15 @@ def create_queue(queue_name, queue_description, guest_tier, orgs): except SQLAlchemyError as ex: raise ex + def update_queue(queue, orgs): try: # Update queue db.session.commit() + if queue.is_default: + _reset_queue_default(queue.id) + # Update org assignments db.session.execute( db.delete(OrganizationJobQueue).filter_by(job_queue_id=queue.id) @@ -152,6 +168,7 @@ def update_queue(queue, orgs): except SQLAlchemyError as ex: raise ex + def delete_queue(queue_id): try: # Clear orgs @@ -164,3 +181,50 @@ def delete_queue(queue_id): db.session.commit() except SQLAlchemyError as ex: raise ex + + +def get_default_queue(): + try: + default_queue = db.session \ + .query(JobQueue) \ + .filter_by(is_default=True) \ + .first() + return default_queue + + except SQLAlchemyError as ex: + raise ex + + +def _reset_queue_default(default_id): + query = "update job_queue set is_default = False where id != {}".format(default_id) + db.session.execute(sqlalchemy.text(query)) + db.session.commit() + + +def validate_or_get_queue(queue: str, job_type: str, user_id: int): + f""" + Validates if the queue name provided is valid and exists if not raises HTTP 400 + If no queue name is provided, it will default to the default job queue. + :param queue: Queue name + :param job_type: Job type + :param user_id: User id to look up available queues + :return: queue + :raises ValueError: If the queue name provided is not valid + """ + valid_queues = get_user_queues(user_id) + + if queue is None or queue == "": + if job_type is None: + default_queue = next(q for q in valid_queues if q.is_default) + return default_queue + recommended_queue = hysds.get_recommended_queue(job_type) + queue = next(q for q in valid_queues if q.queue_name == recommended_queue) + + valid_queue_names = list(map(lambda q: q.queue_name, valid_queues)) + if queue not in valid_queue_names: + raise ValueError(f"User does not have access to {queue}. Valid queues: {valid_queue_names}") + + return next(q for q in valid_queues if q.queue_name == queue) + +def contains_time_limit(queue: job_queue): + return queue is not None and queue.time_limit_minutes is not None and queue.time_limit_minutes > 0 diff --git a/api/utils/ogc_translate.py b/api/utils/ogc_translate.py index 6be1c83..f8d9763 100644 --- a/api/utils/ogc_translate.py +++ b/api/utils/ogc_translate.py @@ -449,7 +449,7 @@ def get_exception(type, origin_process, ex_message): exception = ET.SubElement(response, "ows:Exception") exception.set("exceptionCode", type) exception.set("locator", origin_process) - ET.SubElement(exception, "ows:ExceptionText").text = ex_message + ET.SubElement(exception, "ows:ExceptionText").text = str(ex_message) return tostring(response) diff --git a/test/api/utils/test_hysds_util.py b/test/api/utils/test_hysds_util.py new file mode 100644 index 0000000..3384b5f --- /dev/null +++ b/test/api/utils/test_hysds_util.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import Mock, patch +from api.utils import hysds_util, job_queue +from api import settings +import copy +from requests import Session +import json + + +def mock_session_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + return MockResponse({}, 200) + + +class TestHySDSUtils(unittest.TestCase): + + def setUp(self): + pass + + @patch('requests.Session.get', side_effect=mock_session_get) + def test_get_mozart_job_info(self, mock_session_get): + hysds_util.get_mozart_job_info("someid") + mock_session_get.assert_called_with("{}/job/info".format(settings.MOZART_URL), params={"id": "someid"}) + + def test_remove_double_tag(self): + mozart_response = {"result": {"tags": ["duplicate", "duplicate"]}} + resp = hysds_util.remove_double_tag(mozart_response) + self.assertEqual({"result": {"tags": ["duplicate"]}}, resp) + self.assertNotEqual({"result": {"tags": ["duplicate", "duplicate"]}}, resp) + mozart_response = {"result": {}} + resp = hysds_util.remove_double_tag(mozart_response) + self.assertEqual({"result": {}}, resp) + + def test_add_product_path(self): + self.fail() + + @patch('api.utils.hysds_util.get_recommended_queue') + @patch('api.utils.hysds_util.get_mozart_queues') + def test_validate_queue(self, mock_get_mozart_queues, mock_get_recommended_queue): + mock_get_recommended_queue.return_value = "maap-dps-worker-8gb" + mock_get_mozart_queues.return_value = ["maap-dps-worker-8gb", "maap-dps-worker-16gb"] + queue = "maap-dps-worker-16gb" + job_type = "dummy-job" + user_id = 1 + new_queue = job_queue.validate_or_get_queue(queue, job_type, user_id) + self.assertEqual(queue, new_queue.queue_name) + mock_get_mozart_queues.assert_called() + new_queue = job_queue.validate_or_get_queue("", job_type, user_id) + self.assertEqual("maap-dps-worker-8gb", new_queue.queue_name) + mock_get_recommended_queue.assert_called_with("dummy-job") + with self.assertRaises(ValueError): + job_queue.validate_or_get_queue("invalid_queue", job_type, user_id) + + def test_set_time_limits(self): + params = {"input": "in1", "username": "user"} + expected_params = copy.deepcopy(params) + expected_params.update({"soft_time_limit": "6000", "time_limit": "6000"}) + hysds_util.set_timelimit_for_dps_sandbox(params) + self.assertEqual(expected_params, params) From 3d39bee90afab50392df93b6515a345502395e14 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Thu, 12 Sep 2024 17:40:36 +0000 Subject: [PATCH 17/27] /version 4.1.0rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 05dbc45..7765ee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.2.0a0" +version = "4.1.0rc1" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From bb8805058cbc07dd8d4f1a7a79a57beb4b573ab6 Mon Sep 17 00:00:00 2001 From: Sujen Shah Date: Tue, 17 Sep 2024 08:21:17 -0700 Subject: [PATCH 18/27] Mount maap-dps env file to read api host and dps token from the host (#139) --- api/utils/hysds_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/utils/hysds_util.py b/api/utils/hysds_util.py index 9659bdb..d4dec1f 100644 --- a/api/utils/hysds_util.py +++ b/api/utils/hysds_util.py @@ -222,6 +222,7 @@ def create_job_spec(run_command, inputs, disk_usage, queue_name, verified=False) job_spec["imported_worker_files"] = { "$HOME/.netrc": "/home/ops/.netrc", "$HOME/.aws": "/home/ops/.aws", + "$HOME/verdi/etc/maap-dps.env": "/home/ops/.maap-dps.env", "/tmp": ["/tmp", "rw"] } job_spec["post"] = ["hysds.triage.triage"] From 5a7f72c23f472a80d45c0fffcfe7de8a53834d40 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Tue, 17 Sep 2024 15:21:47 +0000 Subject: [PATCH 19/27] /version 4.1.0rc2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7765ee1..eb88cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0rc1" +version = "4.1.0rc2" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 04971640cd21c90f6d9c4e2f5308651ada33ff65 Mon Sep 17 00:00:00 2001 From: Sujen Shah Date: Tue, 17 Sep 2024 10:15:29 -0700 Subject: [PATCH 20/27] Set job time limit based on the queue time limits if specified (#140) --- api/endpoints/job.py | 5 +++-- api/utils/hysds_util.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/endpoints/job.py b/api/endpoints/job.py index 3adb2d7..4756878 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -63,10 +63,11 @@ def post(self): dedup = "false" if dedup is None else dedup user = get_authorized_user() queue = job_queue.validate_or_get_queue(queue, job_type, user.id) + job_time_limit = hysds_io.get("result").get("soft_time_limit", 86400) if job_queue.contains_time_limit(queue): - hysds.set_timelimit_for_dps_sandbox(params, queue) + job_time_limit = queue.time_limit_minutes response = hysds.mozart_submit_job(job_type=job_type, params=params, dedup=dedup, queue=queue.queue_name, - identifier=identifier) + identifier=identifier, job_time_limit=int(job_time_limit)) logging.info("Mozart Response: {}".format(json.dumps(response))) job_id = response.get("result") diff --git a/api/utils/hysds_util.py b/api/utils/hysds_util.py index d4dec1f..fa48a73 100644 --- a/api/utils/hysds_util.py +++ b/api/utils/hysds_util.py @@ -414,7 +414,8 @@ def get_algorithms(): return maap_algo_list -def mozart_submit_job(job_type, params={}, queue="", dedup="false", identifier="maap-api_submit"): +def mozart_submit_job(job_type, params={}, queue="", dedup="false", identifier="maap-api_submit", + job_time_limit=86400): """ Submit a job to Mozart :param job_type: @@ -422,6 +423,7 @@ def mozart_submit_job(job_type, params={}, queue="", dedup="false", identifier=" :param queue: :param dedup: :param identifier: + :param job_time_limit: :return: """ @@ -447,6 +449,8 @@ def mozart_submit_job(job_type, params={}, queue="", dedup="false", identifier=" params.pop('username', None) job_payload["params"] = json.dumps(params) job_payload["enable_dedup"] = dedup + job_payload["soft_time_limit"] = job_time_limit + job_payload["time_limit"] = job_time_limit logging.info("job payload: {}".format(json.dumps(job_payload))) From 0ff9029b7e2cf75ce8f2a36f97336b09a19689fb Mon Sep 17 00:00:00 2001 From: sujen1412 Date: Tue, 17 Sep 2024 17:15:58 +0000 Subject: [PATCH 21/27] /version 4.1.0rc3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb88cb7..4558cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0rc2" +version = "4.1.0rc3" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From 5d0087e05763de108651b09628d8ba551b7808ae Mon Sep 17 00:00:00 2001 From: Sujen Shah Date: Tue, 17 Sep 2024 11:13:15 -0700 Subject: [PATCH 22/27] Convert time from minutes to seconds (#141) --- api/endpoints/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/endpoints/job.py b/api/endpoints/job.py index 4756878..b170a63 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -65,7 +65,7 @@ def post(self): queue = job_queue.validate_or_get_queue(queue, job_type, user.id) job_time_limit = hysds_io.get("result").get("soft_time_limit", 86400) if job_queue.contains_time_limit(queue): - job_time_limit = queue.time_limit_minutes + job_time_limit = int(queue.time_limit_minutes) * 60 response = hysds.mozart_submit_job(job_type=job_type, params=params, dedup=dedup, queue=queue.queue_name, identifier=identifier, job_time_limit=int(job_time_limit)) From 46efb28830e6e44a528eb30c6c1112a046969606 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Tue, 17 Sep 2024 18:14:04 +0000 Subject: [PATCH 23/27] /version 4.1.0rc4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4558cdb..6196866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0rc3" +version = "4.1.0rc4" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From bee6eed8a04f1657f87e994c64ea0a45eb04c729 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Thu, 19 Sep 2024 09:24:15 -0700 Subject: [PATCH 24/27] Add orgs to user profile --- api/endpoints/members.py | 14 +++++++++++--- api/utils/organization.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/endpoints/members.py b/api/endpoints/members.py index ef7db94..e263e0c 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -4,7 +4,7 @@ from flask import request, jsonify, Response from flask_api import status from sqlalchemy.exc import SQLAlchemyError - +from api.utils.organization import get_member_organizations from api.models.role import Role from api.restplus import api import api.settings as settings @@ -93,6 +93,7 @@ def get(self, key): if member is None: return err_response(msg="No member found with key " + key, code=status.HTTP_404_NOT_FOUND) + member_id = member.id member_schema = MemberSchema() result = json.loads(member_schema.dumps(member)) @@ -100,7 +101,7 @@ def get(self, key): pgt_ticket = db.session \ .query(MemberSession_db) \ .with_entities(MemberSession_db.session_key) \ - .filter_by(member_id=member.id) \ + .filter_by(member_id=member_id) \ .order_by(MemberSession_db.id.desc()) \ .first() @@ -126,6 +127,8 @@ def get(self, key): member_ssh_info_result = json.loads(member_schema.dumps(member)) result = json.loads(json.dumps(dict(result.items() | member_ssh_info_result.items()))) + result['organizations'] = get_member_organizations(member_id) + return result @api.doc(security='ApiKeyAuth') @@ -377,9 +380,14 @@ def get(self): .filter_by(username=authorized_user.username) \ .first() + member_id = member.id + if 'proxy-ticket' in request.headers: member_schema = MemberSchema() - return json.loads(member_schema.dumps(member)) + result = json.loads(member_schema.dumps(member)) + result['organizations'] = get_member_organizations(member_id) + return result + if 'Authorization' in request.headers: return member diff --git a/api/utils/organization.py b/api/utils/organization.py index 2d1b382..8ca91b2 100644 --- a/api/utils/organization.py +++ b/api/utils/organization.py @@ -73,6 +73,23 @@ def get_organizations(): except SQLAlchemyError as ex: raise ex +def get_member_organizations(member_id): + result = [] + + user_orgs = db.session \ + .query(Organization, OrganizationMembership) \ + .filter(Organization.id == OrganizationMembership.org_id) \ + .order_by(Organization.name).all() + + for user_org in user_orgs: + if user_org.OrganizationMembership.member_id == member_id: + result.append({ + 'id': user_org.Organization.id, + 'name': user_org.Organization.name + }) + + return result + def get_organization(org_id): try: org = db.session \ From e9fcd7c4e9c85b9c6e0c54e2163c1b5768772d91 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Thu, 19 Sep 2024 16:25:01 +0000 Subject: [PATCH 25/27] /version 4.1.0rc5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6196866..71061fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0rc4" +version = "4.1.0rc5" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0" From c1ad0e19d39e625ede987403879dff5cb6d8f7bf Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Wed, 25 Sep 2024 13:49:49 -0700 Subject: [PATCH 26/27] Update SQLALCHEMY_ENGINE_OPTIONS settings --- api/maapapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/maapapp.py b/api/maapapp.py index 1025ad2..090de1a 100755 --- a/api/maapapp.py +++ b/api/maapapp.py @@ -39,8 +39,8 @@ app.config['CAS_USERNAME_SESSION_KEY'] = 'cas_token_session_key' app.config['SQLALCHEMY_DATABASE_URI'] = settings.DATABASE_URL app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'isolation_level': 'AUTOCOMMIT', 'pool_size': 300} -app.config['SQLALCHEMY_POOL_TIMEOUT'] = 300 +app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'isolation_level': 'AUTOCOMMIT', 'pool_size': 10, 'pool_pre_ping': True} +app.config['SQLALCHEMY_POOL_TIMEOUT'] = 30 app.app_context().push() db.init_app(app) From b0cbcace6c8a8adb6ef40083c2994b93b798d9a4 Mon Sep 17 00:00:00 2001 From: bsatoriu Date: Wed, 25 Sep 2024 20:51:47 +0000 Subject: [PATCH 27/27] /version 4.1.0rc6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 71061fd..f26eaba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maap-api-nasa" -version = "4.1.0rc5" +version = "4.1.0rc6" description = "NASA Python implementation of the MAAP API specification" authors = ["MAAP-Project Platform "] license = "Apache 2.0"