""" AUTHOR: Khushal P Soonderji DATE: Wednesday, 28th Aug., 2024 OBJECTIVE: To be able to fetch setup credentials and data for any project. This could include things like default values, URLs to assets, etc. While 'data' and 'cred' can have anything held in them, the idea behind giving two services is for the user of this service to be able to organise his setup variables. REFERENCES: N/A DOWNLOADS: N/A NOTES: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # For using Quart: from quart import Blueprint, current_app # My utils: from utils_v2.api.codes import StatusCodes, HttpCodes from utils_v2.api.response import ResponseModel from utils_v2.api.async_quart import ( read_input, log_request_to_mongo, should_not_be_under_maintenance, only_whitelisted_ips, limit_rate, validate_input ) # For asynchronous activities: import asyncio # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # Related to Quart: cred_and_data_bp = Blueprint("int_cnd", __name__) get_api_version = "2.0.0" set_api_version = "2.0.0" update_api_version = "2.0.0" delete_api_version = "2.0.0" # related to the operations of this blueprint: JSON_TYPE_INFO = { "cred": { "collection": "scriptCred" }, "data": { "collection": "scriptData" } } # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** @cred_and_data_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. # Consider this to be a one-time setup for the whole blueprint: pass # --------------------------------------------------------------------------------------------------------------------- @cred_and_data_bp.route("//set", methods = ["POST", "GET"]) @read_input(sanitize_headers = True, sanitize_data = True) @log_request_to_mongo( attr_name = "mongo", log_type = "internalCredData", operation = "set", api_version = set_api_version, log_input = False, log_output = True ) @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @only_whitelisted_ips(attr_name = "whitelisted_ips") @validate_input(mandatory_header_keys = ["X-Script-Id"]) async def set_data( json_type: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, log_id: str = None ): """ To set the credentials for a particular script. If the document exists, it will be overwritten. If the document doesn't exist, it will be created. :param json_type: The choice from one of the fields of 'JSON_TYPE_INFO'. :param inbound_headers: auto-extracted by the decorators from 'async_quart_utils.py'. :param inbound_data: auto-extracted by the decorators from 'async_quart_utils.py'. :param inbound_files: auto-extracted by the decorators from 'async_quart_utils.py'. :param log_id: An identifier for the logs (if logging is enabled). :return: A standard response structure from the function in 'async_quart_utils.py'. """ try: # If an invalid choice was made: if json_type not in JSON_TYPE_INFO.keys(): return ResponseModel( status_code = StatusCodes.FAILED, message = f"invalid url segment '{json_type}'", http_code = HttpCodes.BAD_REQUEST ) # Pre-process the inbound data: inbound_data = inbound_data or {} inbound_data["scriptId"] = inbound_headers["X-Script-Id"] # Make an attempt to set the credentials: success = await current_app.mongo.replace_one( collection = JSON_TYPE_INFO[json_type]["collection"], filter = {"scriptId": inbound_headers["X-Script-Id"]}, replacement = inbound_data, upsert = True ) # Return the response: if success: return ResponseModel(api_version = set_api_version, status_code = StatusCodes.OK) else: return ResponseModel(api_version = set_api_version, status_code = StatusCodes.FAILED) # In case the client terminates the connection prematurely: except asyncio.CancelledError as exception: current_app.printer(exception) return ResponseModel( api_version = set_api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST ) # --------------------------------------------------------------------------------------------------------------------- @cred_and_data_bp.route("//get", methods = ["POST", "GET"]) @read_input(sanitize_headers = True, sanitize_data = True) @log_request_to_mongo( attr_name = "mongo", log_type = "internalCredData", operation = "get", api_version = get_api_version, log_input = True, log_output = False ) @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @only_whitelisted_ips(attr_name = "whitelisted_ips") @validate_input(mandatory_header_keys = ["X-Script-Id"]) async def get_data( json_type: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, log_id: str = None ): """ To retrieve the credentials stored for a specific script. The script's id can be anything set by the programmers. The idea is to have only the script's id stored in the script, and everything else is fetched from the database. This means that we get to store and update everything from one central location. :param json_type: The choice from one of the fields of 'JSON_TYPE_INFO'. :param inbound_headers: auto-extracted by the decorators from 'async_quart.py'. :param inbound_data: auto-extracted by the decorators from 'async_quart.py'. :param inbound_files: auto-extracted by the decorators from 'async_quart.py'. :param log_id: An identifier for the logs (if logging is enabled). :return: A standard response structure from the function in 'async_quart.py'. """ try: # If an invalid choice was made: if json_type not in JSON_TYPE_INFO.keys(): return ResponseModel( status_code = StatusCodes.FAILED, message = f"invalid url segment '{json_type}'", http_code = HttpCodes.BAD_REQUEST ) # Make an attempt to retrieve the credentials: cred_json = await current_app.mongo.find_one( collection = JSON_TYPE_INFO[json_type]["collection"], filter = {"scriptId": inbound_headers["X-Script-Id"]}, projection = {"_id": False, "scriptId": False} ) # In case no result was found: if cred_json is None: return ResponseModel( api_version = get_api_version, status_code = StatusCodes.FAILED, message = "invalid script id" ) # Successfully retrieved: return ResponseModel( api_version = get_api_version, status_code = StatusCodes.OK, data = cred_json ) # In case the client terminates the connection prematurely: except asyncio.CancelledError as exception: current_app.printer(exception) return ResponseModel( api_version = get_api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST ) # --------------------------------------------------------------------------------------------------------------------- @cred_and_data_bp.route("//update", methods = ["POST", "GET"]) @read_input(sanitize_headers = True, sanitize_data = True) @log_request_to_mongo( attr_name = "mongo", log_type = "internalCredData", operation = "update", api_version = update_api_version, log_input = False, log_output = True ) @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @only_whitelisted_ips(attr_name = "whitelisted_ips") @validate_input(mandatory_header_keys = ["X-Script-Id"]) async def update_cred( json_type: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, log_id: str = None ): """ To update values of certain fields for a credentials document. It only updates existing values, does NOT add a new document if the document doesn't already exist. :param json_type: The choice from one of the fields of 'JSON_TYPE_INFO'. :param inbound_headers: auto-extracted by the decorators from 'async_quart_utils.py'. :param inbound_data: auto-extracted by the decorators from 'async_quart_utils.py'. :param inbound_files: auto-extracted by the decorators from 'async_quart_utils.py'. :param log_id: An identifier for the logs (if logging is enabled). :return: A standard response structure from the function in 'async_quart_utils.py'. """ try: # If an invalid choice was made: if json_type not in JSON_TYPE_INFO.keys(): return ResponseModel( status_code = StatusCodes.FAILED, message = f"invalid url segment '{json_type}'", http_code = HttpCodes.BAD_REQUEST ) # Pre-process the inbound data: inbound_data = inbound_data or {} # Prepare the update JSON: update_json = {} if inbound_data.get("unset"): update_json["$unset"] = current_app.mongo.dict_to_dot_notation(inbound_data["unset"]) if inbound_data.get("set"): update_json["$set"] = current_app.mongo.dict_to_dot_notation(inbound_data["set"]) # Make an attempt to set the credentials: success = await current_app.mongo.update_one( collection = JSON_TYPE_INFO[json_type]["collection"], filter = {"scriptId": inbound_headers["X-Script-Id"]}, update = update_json, upsert = False ) # Return the response: if success: return ResponseModel(api_version = update_api_version, status_code = StatusCodes.OK) else: return ResponseModel(api_version = update_api_version, status_code = StatusCodes.FAILED) # In case the client terminates the connection prematurely: except asyncio.CancelledError as exception: current_app.printer(exception) return ResponseModel( api_version = update_api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST ) # --------------------------------------------------------------------------------------------------------------------- @cred_and_data_bp.route("//delete", methods = ["POST", "GET"]) @read_input(sanitize_headers = True, sanitize_data = True) @log_request_to_mongo( attr_name = "mongo", log_type = "internalCredData", operation = "delete", api_version = delete_api_version, log_input = True, log_output = True ) @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @only_whitelisted_ips(attr_name = "whitelisted_ips") @validate_input(mandatory_header_keys = ["X-Script-Id"]) async def delete_data( json_type: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, log_id: str = None ): """ To delete a document for a particular script id. :param json_type: The choice from one of the fields of 'JSON_TYPE_INFO'. :param inbound_headers: auto-extracted by the decorators from 'async_quart.py'. :param inbound_data: auto-extracted by the decorators from 'async_quart.py'. :param inbound_files: auto-extracted by the decorators from 'async_quart.py'. :param log_id: An identifier for the logs (if logging is enabled). :return: A standard response structure from the function in 'async_quart.py'. """ try: # If an invalid choice was made: if json_type not in JSON_TYPE_INFO.keys(): return ResponseModel( status_code = StatusCodes.FAILED, message = f"invalid url segment '{json_type}'", http_code = HttpCodes.BAD_REQUEST ) # Make an attempt to set the credentials: success = await current_app.mongo.delete_one( collection = JSON_TYPE_INFO[json_type]["collection"], filter = {"scriptId": inbound_headers["X-Script-Id"]}, ) # Return the response: if success: return ResponseModel(api_version = delete_api_version, status_code = StatusCodes.OK) else: return ResponseModel(api_version = delete_api_version, status_code = StatusCodes.FAILED) # In case the client terminates the connection prematurely: except asyncio.CancelledError as exception: current_app.printer(exception) return ResponseModel( api_version = delete_api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST ) # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass