diff --git a/edge_cloud_management_api/controllers/app_controllers.py b/edge_cloud_management_api/controllers/app_controllers.py index 604e10d5e0fd477bd7a2112fed73a671392b6a43..b180ca0bfb4054077021e86e648debb5b5bed884 100644 --- a/edge_cloud_management_api/controllers/app_controllers.py +++ b/edge_cloud_management_api/controllers/app_controllers.py @@ -1,7 +1,8 @@ from flask import jsonify, request from pydantic import ValidationError from edge_cloud_management_api.managers.log_manager import logger -from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory +from edge_cloud_management_api.services.edge_cloud_services import PiEdgeAPIClientFactory +from edge_cloud_management_api.services.storage_service import get_zone @@ -99,6 +100,11 @@ def create_app_instance(): if not app_id or not edge_zone_id : return jsonify({"error": "Missing required fields: appId, edgeCloudZoneId, or kubernetesCLusterRef"}), 400 + local_zone = get_zone(edge_zone_id) + if not local_zone: + # TODO: apply federation logic + return 'Zone belongs to federated partner OP. Switching to Federation Manager.' + logger.info(f"Preparing to send deployment request to SRM for appId={app_id}") pi_edge_client_factory = PiEdgeAPIClientFactory() diff --git a/edge_cloud_management_api/controllers/edge_cloud_controller.py b/edge_cloud_management_api/controllers/edge_cloud_controller.py index 5fe9f5e0246c02041fe9bc0d7817a1727abae0e4..7f2748d7b69d043ecbf800b37db040e4abd81da0 100644 --- a/edge_cloud_management_api/controllers/edge_cloud_controller.py +++ b/edge_cloud_management_api/controllers/edge_cloud_controller.py @@ -1,10 +1,17 @@ from flask import jsonify from pydantic import BaseModel, Field, ValidationError from typing import List +from edge_cloud_management_api.configs.env_config import config from edge_cloud_management_api.managers.log_manager import logger -from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory - - +from edge_cloud_management_api.services.edge_cloud_services import PiEdgeAPIClientFactory +from edge_cloud_management_api.services.storage_service import insert_zones + +pi_edge_factory = PiEdgeAPIClientFactory() +api_client = pi_edge_factory.create_pi_edge_api_client() +zones = api_client.edge_cloud_zones() +for zone in zones: + zone['_id'] = zone.get('edgeCloudZoneId') +insert_zones(zones) class EdgeCloudZone(BaseModel): edgeCloudZoneId: str = Field(..., description="Unique identifier of the Edge Cloud Zone") diff --git a/edge_cloud_management_api/controllers/federation_manager_controller.py b/edge_cloud_management_api/controllers/federation_manager_controller.py index 8a39a3780b12658ef0a5b32b6e04bfd7fd73e776..4e6b9ee2f15603c0affc7ad3f3dfe3b25f20b965 100644 --- a/edge_cloud_management_api/controllers/federation_manager_controller.py +++ b/edge_cloud_management_api/controllers/federation_manager_controller.py @@ -1,6 +1,10 @@ -from flask import request, jsonify, Response -import json +from flask import request, jsonify import logging +import connexion +from requests.exceptions import Timeout, ConnectionError +from edge_cloud_management_api.managers.log_manager import logger +import requests + from edge_cloud_management_api.services.federation_services import FederationManagerClientFactory # Factory pattern @@ -8,74 +12,61 @@ factory = FederationManagerClientFactory() federation_client = factory.create_federation_client() - def create_federation(): """POST /partner - Create federation with partner OP.""" - try: - body = request.get_json() - result = federation_client.post_partner(body) - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + body = request.get_json() + token = __get_token() + response = federation_client.post_partner(body, token) + return jsonify(response) def get_federation(federationContextId): """GET /{federationContextId}/partner - Get federation info.""" - try: - result = federation_client.get_partner(federationContextId) - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + token = __get_token() + result = federation_client.get_partner(federationContextId, token) + return jsonify(result) def delete_federation(federationContextId): """DELETE /{federationContextId}/partner - Delete federation.""" - try: - result = federation_client.delete_partner(federationContextId) - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + + token = __get_token() + result = federation_client.delete_partner(federationContextId, token) + return jsonify(result) def get_federation_context_ids(): """GET /fed-context-id - Fetch federationContextId(s).""" - try: - result = federation_client.get_federation_context_ids() - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 - + token = __get_token() + response = federation_client.get_federation_context_ids(token) + return jsonify(response) + def onboard_application_to_partner(federationContextId): """POST /{federationContextId}/application/onboarding - Onboard app.""" - try: - body = request.get_json() - result = federation_client.onboard_application(federationContextId, body) - return jsonify(result), 202 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + body = request.get_json() + token = __get_token() + result = federation_client.onboard_application(federationContextId, body, token) + return jsonify(result) def get_onboarded_app(federationContextId, appId): """GET /{federationContextId}/application/onboarding/app/{appId}""" - try: - result = federation_client.get_onboarded_app(federationContextId, appId) - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + + token = __get_token() + result = federation_client.get_onboarded_app(federationContextId, appId, token) + return jsonify(result) def delete_onboarded_app(federationContextId, appId): """DELETE /{federationContextId}/application/onboarding/app/{appId}""" - try: - result = federation_client.delete_onboarded_app(federationContextId, appId) - return jsonify(result), 200 - except Exception as e: - logging.error(e.args) - return jsonify(result), 500 + + token = __get_token() + result = federation_client.delete_onboarded_app(federationContextId, appId, token) + return jsonify(result) + + +def __get_token(): + bearer = connexion.request.headers['Authorization'] + token = bearer.split()[1] + return token diff --git a/edge_cloud_management_api/controllers/network_functions_controller.py b/edge_cloud_management_api/controllers/network_functions_controller.py index c10fc801b37808adc8ff1b189f29629aca8147be..5750ff5c9a5a64d750e946d1ca6f85e196e67c71 100644 --- a/edge_cloud_management_api/controllers/network_functions_controller.py +++ b/edge_cloud_management_api/controllers/network_functions_controller.py @@ -1,7 +1,7 @@ from flask import jsonify from pydantic import ValidationError #Field from edge_cloud_management_api.managers.log_manager import logger -from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory +from edge_cloud_management_api.services.edge_cloud_services import PiEdgeAPIClientFactory def create_qod_session(body: dict): """ diff --git a/edge_cloud_management_api/controllers/security_controller.py b/edge_cloud_management_api/controllers/security_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..55040f33e63a929c32f3935be09b399f4259f73e --- /dev/null +++ b/edge_cloud_management_api/controllers/security_controller.py @@ -0,0 +1,18 @@ +import os +from jose import JWTError, jwt +from werkzeug.exceptions import Unauthorized + +def decode_token(token:str): + JWT_ISSUER = os.environ.get('JWT_ISSUER') + PUBLIC_KEY = os.environ.get('JWT_PUBLIC_KEY') + try: + return jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256"], issuer=JWT_ISSUER) + except JWTError as e: + raise Unauthorized from e + +def check_oAuth2ClientCredentials(token): + return {'scopes': ['fed-mgmt'], 'uid': 'test_value'} + + +def validate_scope_oAuth2ClientCredentials(required_scopes, token_scopes): + return set(required_scopes).issubset(set(token_scopes)) diff --git a/edge_cloud_management_api/services/pi_edge_services.py b/edge_cloud_management_api/services/edge_cloud_services.py similarity index 96% rename from edge_cloud_management_api/services/pi_edge_services.py rename to edge_cloud_management_api/services/edge_cloud_services.py index 503d7dac04ba550f527c1f869eb0c818357f3c30..4463b6e1ea52f0962d9a570184341c8b2499849e 100644 --- a/edge_cloud_management_api/services/pi_edge_services.py +++ b/edge_cloud_management_api/services/edge_cloud_services.py @@ -1,383 +1,378 @@ -import requests -from edge_cloud_management_api.managers.log_manager import logger -from requests.exceptions import Timeout, ConnectionError -from edge_cloud_management_api.configs.env_config import config - - -proxies = { - "http": config.HTTP_PROXY, - "https": config.HTTP_PROXY, -} - - -class PiEdgeAPIClient: - def __init__(self, base_url, username, password): - self.base_url = base_url - self.username = username - self.password = password - self.token = None - #self.requests_session = self._get_proxy_session(proxies) - - - def _get_proxy_session(self, session_proxies): - session = requests.Session() - session.proxies.update(session_proxies) - return session - - def _authenticate(self): - """ - Private method to login and obtain an authentication token. - This is automatically called when headers are required and token is missing. - """ - login_url = f"{self.base_url}/authentication" - credentials = {"username": self.username, "password": self.password} - - try: - response = self.requests_session.post( - login_url, - json=credentials, - # proxies=proxies, - ) - response.raise_for_status() - - self.token = response.json().get("token") - if not self.token: - raise ValueError("Login failed: No token found") - except requests.exceptions.HTTPError as http_err: - logger.error(f"HTTP error occurred: {http_err}") - except Exception as err: - logger.error(f"Error occurred: {err}") - - def _get_headers(self): - """ - Helper function to return the authorization headers with token. - If token is not available, automatically login. - """ - #if not self.token: - # self._authenticate() - - return { - # "Authorization": f"Bearer {self.token}", - "Content-Type": "application/json", - } - - def get_service_functions_catalogue(self): - """ - Get service function catalogue from the /serviceFunction endpoint. - """ - url = f"{self.base_url}/serviceFunction" - try: - request_headers = self._get_headers() - response = requests.get(url, headers=request_headers, verify=False) - response.raise_for_status() - service_functions = response.json() - if isinstance(service_functions, list): - return service_functions - raise ValueError("Unexpected response from Service Resource manager") - - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - except ValueError as val_err: - return {"error": str(val_err)} - - except Exception as err: - return {"error": f"An unexpected error occurred: {err}"} - - - def submit_app(self, body): - """ - Register app metadata to SRM - """ - url = f"{self.base_url}/serviceFunction" - try: - request_headers = self._get_headers() - response = requests.post(url, headers=request_headers, verify=False, json=body) - response.raise_for_status() - return response.json() - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - def get_app(self, appId): - """ - Get app metadata from SRM - """ - url = f"{self.base_url}/serviceFunction/"+appId - try: - request_headers = self._get_headers() - response = requests.get(url, headers=request_headers, verify=False) - response.raise_for_status() - return response.json() - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - def delete_app(self, appId:str): - """ - Remove app metadata from SRM - """ - url = f"{self.base_url}/serviceFunction/"+appId - try: - response = requests.delete(url,headers=self._get_headers(), verify=False) - response.raise_for_status() - return response.text - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - except Exception as err: - return {"error": f"An unexpected error occurred: {err}"} - - def deploy_service_function(self, data: dict): - """ - Post data to the /deployedServiceFunction endpoint. - """ - url = f"{self.base_url}/deployedServiceFunction" - try: - response = requests.post(url, json=data, headers=self._get_headers(), verify=False) - response.raise_for_status() - return response.json() - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - except Exception as err: - return {"error": f"An unexpected error occurred: {err}"} - - - def get_app_instances(self): - """ - Retrieve all app instances. - """ - url = f"{self.base_url}/deployedServiceFunction" - try: - response = requests.get(url, headers=self._get_headers(), verify=False) - response.raise_for_status() - return response.json() - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - - def delete_app_instance(self, app_instance_id:str): - """ - Remove app instance. - """ - url = f"{self.base_url}/deployedServiceFunction/"+app_instance_id - try: - response = requests.delete(url, headers=self._get_headers(), verify=False) - response.raise_for_status() - return response - except Timeout: - return {"error": "The request to the external API timed out. Please try again later."} - - except ConnectionError: - return {"error": "Failed to connect to the external API service. Service might be unavailable."} - - except requests.exceptions.HTTPError as http_err: - return { - "error": f"HTTP error occurred: {http_err}.", - "status_code": response.status_code, - } - - - def edge_cloud_zones(self): - """ - Get list of edge zones from /node endpoint. - """ - url = f"{self.base_url}/node" - #try: - request_headers = self._get_headers() - response = requests.get(url, headers=request_headers, verify=False) - response.raise_for_status() - nodes = response.json() - if not nodes: - raise ValueError("No edge nodes found") - return nodes - - - def edge_cloud_zone_details(self, zone_id): - """ - Get list of edge zones from /node endpoint. - """ - url = f"{self.base_url}/node/"+zone_id - #try: - request_headers = self._get_headers() - response = requests.get(url, headers=request_headers, verify=False) - response.raise_for_status() - nodes = response.json() - if not nodes: - raise ValueError("No edge nodes found") - return nodes - - def create_qod_session(self, body:dict): - - url = f"{self.base_url}/sessions" - request_headers = self._get_headers() - try: - response = requests.post(url, json=body, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def get_qod_session(self, sessionId: str): - - url = f"{self.base_url}/sessions/"+sessionId - request_headers = self._get_headers() - try: - response = requests.get(url, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def delete_qod_session(self, sessionId: str): - - url = f"{self.base_url}/sessions/"+sessionId - request_headers = self._get_headers() - try: - response = requests.delete(url, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def create_traffic_influence_resource(self, body_dict): - url = f"{self.base_url}/traffic-influences" - request_headers = self._get_headers() - try: - response = requests.post(url, json=body_dict, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def delete_traffic_influence_resource(self, id: str): - url = f"{self.base_url}/traffic-influences/"+id - request_headers = self._get_headers() - try: - response = requests.delete(url, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def get_traffic_influence_resource(self, id: str): - url = f"{self.base_url}/traffic-influences/"+id - request_headers = self._get_headers() - try: - response = requests.get(url, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - def get_all_traffic_influence_resources(self): - url = f"{self.base_url}/traffic-influences/" - request_headers = self._get_headers() - try: - response = requests.get(url, headers=request_headers,verify=False) - response.raise_for_status() - return response - except Exception as e: - logger.info(e.args) - return e.args - - - - -class PiEdgeAPIClientFactory: - """ - Factory class to create instances of PiEdgeAPIClient. - """ - - def __init__(self): - self.default_base_url = config.SRM_HOST - self.default_username = config.PI_EDGE_USERNAME - self.default_password = config.PI_EDGE_PASSWORD - - def create_pi_edge_api_client(self, base_url=None, username=None, password=None): - """ - Factory method to create a new SRMAPIClient instance. - - Args: - base_url (str): The base URL for the SRM API. If None, the default is used. - username (str): The username for authentication. If None, the default is used. - password (str): The password for authentication. If None, the default is used. - - Returns: - PiEdgeAPIClient: A new instance of the PiEdgeAPIClient. - """ - if base_url is None: - base_url = self.default_base_url - if username is None: - username = self.default_username - if password is None: - password = self.default_password - - return PiEdgeAPIClient(base_url=base_url, username=username, password=password) - - -if __name__ == "__main__": - pi_edge_factory = PiEdgeAPIClientFactory() - api_client = pi_edge_factory.create_pi_edge_api_client() - - edge_zones = api_client.edge_cloud_zones() - logger.error("Edge zones:", edge_zones) +import requests +from edge_cloud_management_api.managers.log_manager import logger +from requests.exceptions import Timeout, ConnectionError +from edge_cloud_management_api.configs.env_config import config + + +proxies = { + "http": config.HTTP_PROXY, + "https": config.HTTP_PROXY, +} + +class PiEdgeAPIClient: + def __init__(self, base_url, username, password): + self.base_url = base_url + self.username = username + self.password = password + self.token = None + + + def _get_proxy_session(self, session_proxies): + session = requests.Session() + session.proxies.update(session_proxies) + return session + + def _authenticate(self): + """ + Private method to login and obtain an authentication token. + This is automatically called when headers are required and token is missing. + """ + login_url = f"{self.base_url}/authentication" + credentials = {"username": self.username, "password": self.password} + + try: + response = self.requests_session.post( + login_url, + json=credentials, + # proxies=proxies, + ) + response.raise_for_status() + + self.token = response.json().get("token") + if not self.token: + raise ValueError("Login failed: No token found") + except requests.exceptions.HTTPError as http_err: + logger.error(f"HTTP error occurred: {http_err}") + except Exception as err: + logger.error(f"Error occurred: {err}") + + def _get_headers(self): + """ + Helper function to return the authorization headers with token. + If token is not available, automatically login. + """ + #if not self.token: + # self._authenticate() + + return { + # "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + + def get_service_functions_catalogue(self): + """ + Get service function catalogue from the /serviceFunction endpoint. + """ + url = f"{self.base_url}/serviceFunction" + try: + request_headers = self._get_headers() + response = requests.get(url, headers=request_headers, verify=False) + response.raise_for_status() + service_functions = response.json() + if isinstance(service_functions, list): + return service_functions + raise ValueError("Unexpected response from Service Resource manager") + + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + except ValueError as val_err: + return {"error": str(val_err)} + + except Exception as err: + return {"error": f"An unexpected error occurred: {err}"} + + + def submit_app(self, body): + """ + Register app metadata to SRM + """ + url = f"{self.base_url}/serviceFunction" + try: + request_headers = self._get_headers() + response = requests.post(url, headers=request_headers, verify=False, json=body) + response.raise_for_status() + return response.json() + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + def get_app(self, appId): + """ + Get app metadata from SRM + """ + url = f"{self.base_url}/serviceFunction/"+appId + try: + request_headers = self._get_headers() + response = requests.get(url, headers=request_headers, verify=False) + response.raise_for_status() + return response.json() + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + def delete_app(self, appId:str): + """ + Remove app metadata from SRM + """ + url = f"{self.base_url}/serviceFunction/"+appId + try: + response = requests.delete(url,headers=self._get_headers(), verify=False) + response.raise_for_status() + return response.text + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + except Exception as err: + return {"error": f"An unexpected error occurred: {err}"} + + def deploy_service_function(self, data: dict): + """ + Post data to the /deployedServiceFunction endpoint. + """ + url = f"{self.base_url}/deployedServiceFunction" + try: + response = requests.post(url, json=data, headers=self._get_headers(), verify=False) + response.raise_for_status() + return response.json() + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + except Exception as err: + return {"error": f"An unexpected error occurred: {err}"} + + + def get_app_instances(self): + """ + Retrieve all app instances. + """ + url = f"{self.base_url}/deployedServiceFunction" + try: + response = requests.get(url, headers=self._get_headers(), verify=False) + response.raise_for_status() + return response.json() + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + + def delete_app_instance(self, app_instance_id:str): + """ + Remove app instance. + """ + url = f"{self.base_url}/deployedServiceFunction/"+app_instance_id + try: + response = requests.delete(url, headers=self._get_headers(), verify=False) + response.raise_for_status() + return response + except Timeout: + return {"error": "The request to the external API timed out. Please try again later."} + + except ConnectionError: + return {"error": "Failed to connect to the external API service. Service might be unavailable."} + + except requests.exceptions.HTTPError as http_err: + return { + "error": f"HTTP error occurred: {http_err}.", + "status_code": response.status_code, + } + + + def edge_cloud_zones(self): + """ + Get list of edge zones from /node endpoint. + """ + url = f"{self.base_url}/node" + #try: + request_headers = self._get_headers() + response = requests.get(url, headers=request_headers, verify=False) + response.raise_for_status() + nodes = response.json() + if not nodes: + raise ValueError("No edge nodes found") + return nodes + + + def edge_cloud_zone_details(self, zone_id): + """ + Get list of edge zones from /node endpoint. + """ + url = f"{self.base_url}/node/"+zone_id + #try: + request_headers = self._get_headers() + response = requests.get(url, headers=request_headers, verify=False) + response.raise_for_status() + nodes = response.json() + if not nodes: + raise ValueError("No edge nodes found") + return nodes + + def create_qod_session(self, body:dict): + + url = f"{self.base_url}/sessions" + request_headers = self._get_headers() + try: + response = requests.post(url, json=body, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def get_qod_session(self, sessionId: str): + + url = f"{self.base_url}/sessions/"+sessionId + request_headers = self._get_headers() + try: + response = requests.get(url, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def delete_qod_session(self, sessionId: str): + + url = f"{self.base_url}/sessions/"+sessionId + request_headers = self._get_headers() + try: + response = requests.delete(url, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def create_traffic_influence_resource(self, body_dict): + url = f"{self.base_url}/traffic-influences" + request_headers = self._get_headers() + try: + response = requests.post(url, json=body_dict, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def delete_traffic_influence_resource(self, id: str): + url = f"{self.base_url}/traffic-influences/"+id + request_headers = self._get_headers() + try: + response = requests.delete(url, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def get_traffic_influence_resource(self, id: str): + url = f"{self.base_url}/traffic-influences/"+id + request_headers = self._get_headers() + try: + response = requests.get(url, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + + def get_all_traffic_influence_resources(self): + url = f"{self.base_url}/traffic-influences/" + request_headers = self._get_headers() + try: + response = requests.get(url, headers=request_headers,verify=False) + response.raise_for_status() + return response + except Exception as e: + logger.info(e.args) + return e.args + +class PiEdgeAPIClientFactory: + """ + Factory class to create instances of PiEdgeAPIClient. + """ + + def __init__(self): + self.default_base_url = config.SRM_HOST + self.default_username = config.PI_EDGE_USERNAME + self.default_password = config.PI_EDGE_PASSWORD + + def create_pi_edge_api_client(self, base_url=None, username=None, password=None): + """ + Factory method to create a new SRMAPIClient instance. + + Args: + base_url (str): The base URL for the SRM API. If None, the default is used. + username (str): The username for authentication. If None, the default is used. + password (str): The password for authentication. If None, the default is used. + + Returns: + PiEdgeAPIClient: A new instance of the PiEdgeAPIClient. + """ + if base_url is None: + base_url = self.default_base_url + if username is None: + username = self.default_username + if password is None: + password = self.default_password + + return PiEdgeAPIClient(base_url=base_url, username=username, password=password) + + +if __name__ == "__main__": + pi_edge_factory = PiEdgeAPIClientFactory() + api_client = pi_edge_factory.create_pi_edge_api_client() + + edge_zones = api_client.edge_cloud_zones() + logger.error("Edge zones:", edge_zones) diff --git a/edge_cloud_management_api/services/federation_services.py b/edge_cloud_management_api/services/federation_services.py index 92be7f48ce6101eb68f5c6116fb497cb6102936c..3d0deb05abbb81dc5a62f502823fef770cf8aa88 100644 --- a/edge_cloud_management_api/services/federation_services.py +++ b/edge_cloud_management_api/services/federation_services.py @@ -3,27 +3,30 @@ import requests from requests.exceptions import Timeout, ConnectionError from edge_cloud_management_api.configs.env_config import config from edge_cloud_management_api.managers.log_manager import logger -from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory +from edge_cloud_management_api.services.edge_cloud_services import PiEdgeAPIClientFactory class FederationManagerClient: def __init__(self, base_url=None): self.base_url = base_url or config.FEDERATION_MANAGER_HOST - def _get_headers(self): - return { - "Content-Type": "application/json", - "Accept": "application/json" - } + def _get_headers(self, token): + headers = {} + if token is not None: + headers['Authorization'] = 'Bearer '+token + + headers['Content-Type'] = 'application/json' + headers['Accept'] = 'application/json' + return headers - def post_partner(self, data: dict): + def post_partner(self, data: dict, token: str): url = f"{self.base_url}/partner" try: - response = requests.post(url, json=data, headers=self._get_headers(), timeout=10) + response = requests.post(url, json=data, headers=self._get_headers(token), timeout=10) response.raise_for_status() return response.json() except Timeout: logger.error("POST /partner timed out") - return {"error": "Request timed out"} + return {"error": "Request timed out"}, 408 except ConnectionError: logger.error("POST /partner connection error") return {"error": "Connection error"} @@ -34,10 +37,10 @@ class FederationManagerClient: logger.error(f"POST /partner unexpected error: {e}") return {"error": str(e)} - def get_partner(self, federation_context_id: str): + def get_partner(self, federation_context_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/partner" try: - response = requests.get(url, headers=self._get_headers(), timeout=10) + response = requests.get(url, headers=self._get_headers(token), timeout=10) response.raise_for_status() return response.json() except Timeout: @@ -53,10 +56,10 @@ class FederationManagerClient: logger.error(f"GET /{id}/partner unexpected error: {e}") return {"error": str(e)} - def delete_partner(self, federation_context_id: str): + def delete_partner(self, federation_context_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/partner" try: - response = requests.delete(url, headers=self._get_headers(), timeout=10) + response = requests.delete(url, headers=self._get_headers(token), timeout=10) if response.content: return response.json() return {"status": response.status_code} @@ -73,10 +76,10 @@ class FederationManagerClient: logger.error(f"DELETE /{id}/partner unexpected error: {e}") return {"error": str(e)} - def get_federation_context_ids(self): + def get_federation_context_ids(self, token: str): url = f"{self.base_url}/fed-context-id" try: - response = requests.get(url, headers=self._get_headers(), timeout=10) + response = requests.get(url, headers=self._get_headers(token), timeout=10) response.raise_for_status() return response.json() except Timeout: @@ -87,12 +90,13 @@ class FederationManagerClient: return {"error": "Connection error"} except requests.exceptions.HTTPError as http_err: logger.error(f"GET /fed-context-id HTTP error: {http_err}") - return {"error": str(http_err), "status_code": response.status_code} + if response.status_code==404: + return {'erorr': http_err}, 404 except Exception as e: logger.error(f"GET /fed-context-id unexpected error: {e}") return {"error": str(e)} - def onboard_application(self, federation_context_id: str, body: dict): + def onboard_application(self, federation_context_id: str, body: dict, token: str): url = f"{self.base_url}/{federation_context_id}/application/onboarding" try: response = requests.post(url, headers=self._get_headers(), json=body, timeout=10) @@ -112,10 +116,10 @@ class FederationManagerClient: return {"error": str(e)} - def get_onboarded_app(self, federation_context_id: str, app_id: str): + def get_onboarded_app(self, federation_context_id: str, app_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/application/onboarding/app/{app_id}" try: - response = requests.get(url, headers=self._get_headers(), timeout=10) + response = requests.get(url, headers=self._get_headers(token), timeout=10) response.raise_for_status() return response.json() except Timeout: @@ -131,7 +135,7 @@ class FederationManagerClient: logger.error(f"GET onboarded app unexpected error: {e}") return {"error": str(e), "status_code": 500} - def delete_onboarded_app(self, federation_context_id: str, app_id: str): + def delete_onboarded_app(self, federation_context_id: str, app_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/application/onboarding/app/{app_id}" try: response = requests.delete(url, headers=self._get_headers(), timeout=10) diff --git a/edge_cloud_management_api/services/storage_service.py b/edge_cloud_management_api/services/storage_service.py new file mode 100644 index 0000000000000000000000000000000000000000..38d80b59703626117aea6a38655fa8b522e17b38 --- /dev/null +++ b/edge_cloud_management_api/services/storage_service.py @@ -0,0 +1,20 @@ +from edge_cloud_management_api.configs.env_config import config +import pymongo + +storage_url = mongo_host = config.MONGO_URI +mydb_mongo = 'oeg_storage' + +def insert_zones(zone_list: list): + collection = "zones" + myclient = pymongo.MongoClient(storage_url) + mydbmongo = myclient[mydb_mongo] + col = mydbmongo[collection] + col.insert_many(zone_list) + +def get_zone(zone_id: str): + collection = "zones" + myclient = pymongo.MongoClient(storage_url) + mydbmongo = myclient[mydb_mongo] + col = mydbmongo[collection] + zone = col.find_one({'_id': zone_id}) + return zone \ No newline at end of file diff --git a/edge_cloud_management_api/specification/openapi.yaml b/edge_cloud_management_api/specification/openapi.yaml index e2d5c54a22a96d5eba5aaf193125d4e8889239d4..3370f0c6d1e185437eafb1d744606b9322d0faf4 100644 --- a/edge_cloud_management_api/specification/openapi.yaml +++ b/edge_cloud_management_api/specification/openapi.yaml @@ -1,11 +1,11 @@ --- openapi: 3.0.3 info: - title: Edge Application Management API - version: 0.9.3-wip + title: Open Exposure Gateway API + version: 1.0.1-wip description: | - Edge Application Management API allows API consumers to manage the - Life Cycle of an Application and to Discover Edge Cloud Zones. + Open Exposure Gateway API allows API consumers to manage the + Life Cycle of an Application, Discover Edge Cloud Zones and request Network Resources. # Overview The reference scenario foresees a distributed Telco Edge Cloud where any Application Delevoper, known as an Application Provider, can host and @@ -149,20 +149,12 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html externalDocs: - description: Product documentation at Camara + description: Product documentation at Camara0 url: https://github.com/camaraproject/EdgeCloud servers: - url: http://vitrualserver:8080/oeg/1.0.0 -tags: - - name: Application - description: Application and Application Instance Lice Cycle Management - - name: Edge Cloud - description: Edge Cloud Zones Availability - - name: FederationManagement - description: Federation Manager - paths: /apps: post: @@ -823,6 +815,10 @@ paths: tags: - FederationManagement summary: Creates one direction federation with partner operator platform. + security: + - oAuth2ClientCredentials: + - fed-mgmt + operationId: edge_cloud_management_api.controllers.federation_manager_controller.create_federation requestBody: content: @@ -871,14 +867,14 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - "409": - description: Conflict + "408": + description: Timeout content: application/problem+json: schema: - $ref: '#/components/schemas/ProblemDetails' - "422": - description: Unprocessable Entity + $ref: '#/components/schemas/ProblemDetails' + "409": + description: Conflict content: application/problem+json: schema: @@ -889,20 +885,6 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - "503": - description: Service Unavailable - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - "520": - description: Web Server Returned an Unknown Error - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - default: - description: Generic Error callbacks: onPartnerStatusEvent: '{$request.body#/partnerStatusLink }': @@ -1064,6 +1046,9 @@ paths: \ The response shall provide info about the zones offered by the partner,\ \ partner OP network codes, information about edge discovery and LCM service\ \ etc." + security: + - oAuth2ClientCredentials: + - fed-mgmt operationId: edge_cloud_management_api.controllers.federation_manager_controller.get_federation parameters: - name: federationContextId @@ -1080,12 +1065,6 @@ paths: application/json: schema: $ref: '#/components/schemas/inline_response_200_1' - "400": - description: Bad request - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' "401": description: Unauthorized content: @@ -1098,42 +1077,19 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - "409": - description: Conflict - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - "422": - description: Unprocessable Entity - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' "500": description: Internal Server Error content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - "503": - description: Service Unavailable - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - "520": - description: Web Server Returned an Unknown Error - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - default: - description: Generic Error delete: tags: - FederationManagement summary: Remove existing federation with the partner OP + security: + - oAuth2ClientCredentials: + - fed-mgmt operationId: edge_cloud_management_api.controllers.federation_manager_controller.delete_federation parameters: - name: federationContextId @@ -1146,56 +1102,32 @@ paths: responses: "200": description: Federation removed successfully - "400": - description: Bad request - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' "401": description: Unauthorized - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - "409": - description: Conflict content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - - "422": - description: Unprocessable Entity + "404": + description: Not Found content: application/problem+json: schema: - $ref: '#/components/schemas/ProblemDetails' + $ref: '#/components/schemas/ProblemDetails' "500": description: Internal Server Error - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - "503": - description: Service Unavailable content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' - "520": - description: Web Server Returned an Uknown Error - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - default: - description: Generic Error /fed-context-id: get: tags: - FederationManagement summary: Retrieves the existing federationContextId with partner operator platform. + security: + - oAuth2ClientCredentials: + - fed-mgmt operationId: edge_cloud_management_api.controllers.federation_manager_controller.get_federation_context_ids responses: "200": @@ -1240,10 +1172,22 @@ paths: components: # securitySchemes: - # openId: - # description: OpenID Provider Configuration Information. - # type: openIdConnect - # openIdConnectUrl: https://example.com/.well-known/openid-configuration + # jwt: # arbitrary name for the security scheme + # type: http + # scheme: bearer + # bearerFormat: JWT + # x-bearerInfoFunc: edge_cloud_management_api.controllers.security_controller.decode_token + securitySchemes: + oAuth2ClientCredentials: + type: oauth2 + flows: + clientCredentials: + tokenUrl: http://isiath.duckdns.org:8081//realms/federation/protocol/openid-connect/token + scopes: + fed-mgmt: Access to the federation APIs + x-tokenInfoFunc: edge_cloud_management_api.controllers.security_controller.check_oAuth2ClientCredentials + x-scopeValidateFunc: edge_cloud_management_api.controllers.security_controller.validate_scope_oAuth2ClientCredentials + parameters: x-correlator: name: x-correlator diff --git a/oeg-deployment.yaml b/oeg-deployment.yaml deleted file mode 100644 index be86353f28194849d42e6aa01a410086942f8c7a..0000000000000000000000000000000000000000 --- a/oeg-deployment.yaml +++ /dev/null @@ -1,122 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegmongo - name: oegmongo -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: oegmongo - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - #io.kompose.network/netEMPkub: "true" - io.kompose.service: oegmongo - spec: - containers: - - image: mongo - name: oegmongo - ports: - - containerPort: 27017 - resources: {} - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegmongo - name: oegmongo -spec: - type: ClusterIP - ports: - - name: "27018" - port: 27018 - targetPort: 27017 - selector: - io.kompose.service: oegmongo -status: - loadBalancer: {} - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegcontroller - name: oegcontroller -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: oegcontroller - strategy: {} - template: - metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegcontroller - spec: - containers: - - env: - - name: MONGO_URI - value: mongodb://oegmongo/sample_db?authSource=admin - - name: SRM_HOST - value: http://srm:8080/srm/1.0.0 - - name: FEDERATION_MANAGER_HOST - value: http://federation-manager:8989 - image: ghcr.io/sunriseopenoperatorplatform/oeg/oeg - name: oegcontroller - ports: - - containerPort: 8080 - resources: {} - imagePullPolicy: Always - restartPolicy: Always - -status: {} ---- -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oeg - name: oeg -spec: - type: NodePort - ports: - - name: "8080" - nodePort: 32414 - port: 8080 - targetPort: 8080 - selector: - io.kompose.service: oegcontroller -status: - loadBalancer: {} - diff --git a/requirements.txt b/requirements.txt index 447a804e834b6d4fc9cb8bb78961d327e75f18ff..f11bd9ac98ee368bb00db9058cec20fdfd363dc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,450 +1,67 @@ # This file was autogenerated by uv via the following command: # uv export #-e . -a2wsgi==1.10.7 \ - --hash=sha256:6d7c602fb1f9cc6afc6c6d0558d3354f3c7aa281e73e6dc9e001dbfc1d9e80cf \ - --hash=sha256:ce462ff7e1daac0bc57183c6f800f09a71c2a7a98ddd5cdeca149e3eabf3338e -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 -anyio==4.7.0 \ - --hash=sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48 \ - --hash=sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352 -asgiref==3.8.1 \ - --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \ - --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590 -attrs==24.2.0 \ - --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ - --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 -blinker==1.9.0 \ - --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ - --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc -cachetools==5.5.0 \ - --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ - --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a -certifi==2024.8.30 \ - --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ - --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 -chardet==5.2.0 \ - --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ - --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 -charset-normalizer==3.4.0 \ - --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ - --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ - --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ - --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ - --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ - --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ - --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ - --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ - --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ - --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ - --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ - --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ - --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ - --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ - --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ - --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ - --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ - --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ - --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ - --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ - --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ - --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ - --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ - --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ - --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ - --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ - --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ - --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ - --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ - --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ - --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ - --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -connexion==3.1.0 \ - --hash=sha256:66a44580991f53955b6e409a84fa9fa65c7ca4db52dc217b49cd35c201066083 \ - --hash=sha256:e92b6d0412eb54b3b69f2516b93d982a06b0e23f6d5c1ab94257c55d365f63ce -distlib==0.3.9 \ - --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ - --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 -dnspython==2.7.0 \ - --hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 \ - --hash=sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1 -filelock==3.16.1 \ - --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ - --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 -flask==3.1.0 \ - --hash=sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac \ - --hash=sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136 -h11==0.14.0 \ - --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ - --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 -hatchling==1.26.3 \ - --hash=sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a \ - --hash=sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846 -httpcore==1.0.7 \ - --hash=sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c \ - --hash=sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd -httptools==0.6.4 \ - --hash=sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2 \ - --hash=sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8 \ - --hash=sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3 \ - --hash=sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5 \ - --hash=sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0 \ - --hash=sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071 \ - --hash=sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c \ - --hash=sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1 \ - --hash=sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44 \ - --hash=sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083 \ - --hash=sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660 \ - --hash=sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970 \ - --hash=sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2 \ - --hash=sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81 \ - --hash=sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f -httpx==0.28.1 \ - --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ - --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 -inflection==0.5.1 \ - --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ - --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 -iniconfig==2.0.0 \ - --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ - --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d -jsonschema==4.23.0 \ - --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ - --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 -jsonschema-specifications==2024.10.1 \ - --hash=sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272 \ - --hash=sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf -markupsafe==3.0.2 \ - --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ - --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ - --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ - --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ - --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ - --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ - --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ - --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ - --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ - --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ - --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ - --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ - --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ - --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ - --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ - --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ - --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ - --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ - --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ - --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ - --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ - --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ - --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ - --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ - --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ - --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ - --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ - --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ - --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ - --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ - --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 -mongomock==4.3.0 \ - --hash=sha256:32667b79066fabc12d4f17f16a8fd7361b5f4435208b3ba32c226e52212a8c30 \ - --hash=sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e -mypy==1.14.1 \ - --hash=sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd \ - --hash=sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14 \ - --hash=sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e \ - --hash=sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6 \ - --hash=sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107 \ - --hash=sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11 \ - --hash=sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a \ - --hash=sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b \ - --hash=sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255 \ - --hash=sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1 \ - --hash=sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9 \ - --hash=sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9 \ - --hash=sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34 \ - --hash=sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89 -mypy-extensions==1.0.0 \ - --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ - --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 -platformdirs==4.3.6 \ - --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ - --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb -pluggy==1.5.0 \ - --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ - --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 -pydantic==2.10.3 \ - --hash=sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d \ - --hash=sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9 -pydantic-core==2.27.1 \ - --hash=sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529 \ - --hash=sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc \ - --hash=sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c \ - --hash=sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac \ - --hash=sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2 \ - --hash=sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a \ - --hash=sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089 \ - --hash=sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107 \ - --hash=sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf \ - --hash=sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5 \ - --hash=sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23 \ - --hash=sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02 \ - --hash=sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235 \ - --hash=sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16 \ - --hash=sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c \ - --hash=sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35 \ - --hash=sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737 \ - --hash=sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05 \ - --hash=sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb \ - --hash=sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e \ - --hash=sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f \ - --hash=sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960 \ - --hash=sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08 \ - --hash=sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337 \ - --hash=sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51 \ - --hash=sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381 \ - --hash=sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb \ - --hash=sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073 \ - --hash=sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae -pymongo==4.10.1 \ - --hash=sha256:0783e0c8e95397c84e9cf8ab092ab1e5dd7c769aec0ef3a5838ae7173b98dea0 \ - --hash=sha256:11280809e5dacaef4971113f0b4ff4696ee94cfdb720019ff4fa4f9635138252 \ - --hash=sha256:2e3a593333e20c87415420a4fb76c00b7aae49b6361d2e2205b6fece0563bf40 \ - --hash=sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708 \ - --hash=sha256:4924355245a9c79f77b5cda2db36e0f75ece5faf9f84d16014c0a297f6d66786 \ - --hash=sha256:544890085d9641f271d4f7a47684450ed4a7344d6b72d5968bfae32203b1bb7c \ - --hash=sha256:594dd721b81f301f33e843453638e02d92f63c198358e5a0fa8b8d0b1218dabc \ - --hash=sha256:6fb6a72e88df46d1c1040fd32cd2d2c5e58722e5d3e31060a0393f04ad3283de \ - --hash=sha256:72e2ace7456167c71cfeca7dcb47bd5dceda7db2231265b80fc625c5e8073186 \ - --hash=sha256:7bd26b2aec8ceeb95a5d948d5cc0f62b0eb6d66f3f4230705c1e3d3d2c04ec76 \ - --hash=sha256:8ad05eb9c97e4f589ed9e74a00fcaac0d443ccd14f38d1258eb4c39a35dd722b \ - --hash=sha256:90bc6912948dfc8c363f4ead54d54a02a15a7fee6cfafb36dc450fc8962d2cb7 \ - --hash=sha256:a9de02be53b6bb98efe0b9eda84ffa1ec027fcb23a2de62c4f941d9a2f2f3330 \ - --hash=sha256:dcc07b1277e8b4bf4d7382ca133850e323b7ab048b8353af496d050671c7ac52 \ - --hash=sha256:e5d55f2a82e5eb23795f724991cac2bffbb1c0f219c0ba3bf73a835f97f1bb2e \ - --hash=sha256:e974ab16a60be71a8dfad4e5afccf8dd05d41c758060f5d5bda9a758605d9a5d \ - --hash=sha256:ee4c86d8e6872a61f7888fc96577b0ea165eb3bdb0d841962b444fa36001e2bb \ - --hash=sha256:fb104c3c2a78d9d85571c8ac90ec4f95bca9b297c6eee5ada71fabf1129e1674 \ - --hash=sha256:fbedc4617faa0edf423621bb0b3b8707836687161210d470e69a4184be9ca011 -pyproject-api==1.8.0 \ - --hash=sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228 \ - --hash=sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496 -pytest==8.3.4 \ - --hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6 \ - --hash=sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761 -python-dotenv==1.0.1 \ - --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ - --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a -python-multipart==0.0.19 \ - --hash=sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc \ - --hash=sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d -pytz==2024.2 \ - --hash=sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a \ - --hash=sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725 -pyyaml==6.0.2 \ - --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ - --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ - --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ - --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ - --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ - --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ - --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ - --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ - --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ - --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ - --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ - --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ - --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ - --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ - --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ - --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ - --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ - --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ - --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba -referencing==0.35.1 \ - --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ - --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 -rpds-py==0.22.3 \ - --hash=sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518 \ - --hash=sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059 \ - --hash=sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61 \ - --hash=sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5 \ - --hash=sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56 \ - --hash=sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd \ - --hash=sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b \ - --hash=sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4 \ - --hash=sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d \ - --hash=sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1 \ - --hash=sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e \ - --hash=sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc \ - --hash=sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38 \ - --hash=sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b \ - --hash=sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c \ - --hash=sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83 \ - --hash=sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1 \ - --hash=sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627 \ - --hash=sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16 \ - --hash=sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45 \ - --hash=sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730 \ - --hash=sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25 \ - --hash=sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7 \ - --hash=sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f \ - --hash=sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd \ - --hash=sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333 \ - --hash=sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9 \ - --hash=sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4 \ - --hash=sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d \ - --hash=sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15 \ - --hash=sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84 \ - --hash=sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e \ - --hash=sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf \ - --hash=sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b \ - --hash=sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2 \ - --hash=sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3 \ - --hash=sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130 \ - --hash=sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b \ - --hash=sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de \ - --hash=sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e -ruff==0.8.2 \ - --hash=sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f \ - --hash=sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea \ - --hash=sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248 \ - --hash=sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d \ - --hash=sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f \ - --hash=sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29 \ - --hash=sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22 \ - --hash=sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0 \ - --hash=sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1 \ - --hash=sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58 \ - --hash=sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5 \ - --hash=sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d \ - --hash=sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897 \ - --hash=sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa \ - --hash=sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93 \ - --hash=sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5 \ - --hash=sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c \ - --hash=sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8 -sentinels==1.0.0 \ - --hash=sha256:7be0704d7fe1925e397e92d18669ace2f619c92b5d4eb21a89f31e026f9ff4b1 -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc -starlette==0.41.3 \ - --hash=sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835 \ - --hash=sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7 -swagger-ui-bundle==1.1.0 \ - --hash=sha256:20673c3431c8733d5d1615ecf79d9acf30cff75202acaf21a7d9c7f489714529 \ - --hash=sha256:f7526f7bb99923e10594c54247265839bec97e96b0438561ac86faf40d40dd57 -tox==4.23.2 \ - --hash=sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38 \ - --hash=sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c -trove-classifiers==2024.10.21.16 \ - --hash=sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be \ - --hash=sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3 -typing-extensions==4.12.2 \ - --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ - --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 -urllib3==2.2.3 \ - --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ - --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 -uvicorn==0.32.1 \ - --hash=sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e \ - --hash=sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175 -uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 -virtualenv==20.28.1 \ - --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ - --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 -watchfiles==1.0.0 \ - --hash=sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6 \ - --hash=sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f \ - --hash=sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107 \ - --hash=sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285 \ - --hash=sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab \ - --hash=sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e \ - --hash=sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3 \ - --hash=sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945 \ - --hash=sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb \ - --hash=sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c \ - --hash=sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82 \ - --hash=sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234 \ - --hash=sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025 \ - --hash=sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940 \ - --hash=sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152 \ - --hash=sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514 \ - --hash=sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936 \ - --hash=sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359 \ - --hash=sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81 \ - --hash=sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525 \ - --hash=sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc \ - --hash=sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545 \ - --hash=sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941 \ - --hash=sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00 \ - --hash=sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7 \ - --hash=sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98 -websockets==14.1 \ - --hash=sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59 \ - --hash=sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6 \ - --hash=sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0 \ - --hash=sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9 \ - --hash=sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b \ - --hash=sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8 \ - --hash=sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4 \ - --hash=sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e \ - --hash=sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3 \ - --hash=sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058 \ - --hash=sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da \ - --hash=sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2 \ - --hash=sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a \ - --hash=sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0 \ - --hash=sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d \ - --hash=sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7 \ - --hash=sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f \ - --hash=sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a \ - --hash=sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58 \ - --hash=sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45 \ - --hash=sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707 \ - --hash=sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9 \ - --hash=sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05 \ - --hash=sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 +a2wsgi==1.10.7 +annotated-types==0.7.0 +anyio==4.7.0 +attrs==24.2.0 +blinker==1.9.0 +cachetools==5.5.0 +certifi==2024.8.30 +chardet==5.2.0 +charset-normalizer==3.4.0 +click==8.1.7 +colorama==0.4.6 +connexion==3.1.0 +distlib==0.3.9 +dnspython==2.7.0 +filelock==3.16.1 +flask==3.1.0 +h11==0.14.0 +hatchling==1.26.3 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +inflection==0.5.1 +iniconfig==2.0.0 +itsdangerous==2.2.0 +jinja2==3.1.4 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +markupsafe==3.0.2 +mongomock==4.3.0 +mypy==1.14.1 +mypy-extensions==1.0.0 +packaging==24.2 +pathspec==0.12.1 +platformdirs==4.3.6 +pluggy==1.5.0 +pydantic==2.10.3 +pydantic-core==2.27.1 +pymongo==4.10.1 +pyproject-api==1.8.0 +pytest==8.3.4 +python-dotenv==1.0.1 +python-multipart==0.0.19 +pytz==2024.2 +pyyaml==6.0.2 +referencing==0.35.1 +requests==2.32.3 +rpds-py==0.22.3 +ruff==0.8.2 +sentinels==1.0.0 +sniffio==1.3.1 +starlette==0.41.3 +swagger-ui-bundle==1.1.0 +tox==4.23.2 +trove-classifiers==2024.10.21.16 +typing-extensions==4.12.2 +urllib3==2.2.3 +uvicorn==0.32.1 +uvloop==0.21.0 +virtualenv==20.28.1 +watchfiles==1.0.0 +websockets==14.1 +werkzeug==3.1.3 +python-jose[cryptography]==3.5.0 \ No newline at end of file