""" AUTHOR: Khushal P Soonderji DATE: Fri, 14th Nov, 2025 OBJECTIVE: This is the central location for the Quart module. We define the app here, and import and attach all blueprints here. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append("blueprints") sys.path.append("") # For system level activities: import gc import os import psutil # For using Quart: from quart import Quart, current_app from quart_cors import cors # My utils: from utils_v2.date_time import date_time from utils_v2.system import files from utils_v2.api.async_quart import ( set_api_version, log_request_to_mongo, ) # Shared elements: from backend.shared import constants # To make REST API calls: import httpx # For asynchronous activities: import asyncio # For debugging: from icecream import IceCreamDebugger # All the blueprints: from backend.api.blueprints.reports.in_out_report import in_out_report_bp from backend.api.blueprints.reports.muster_roll import muster_roll_report_bp # The helpers: from backend.api.helpers import muster_roll # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # Quart related: MODULE_BASE = constants.MODULE_NAME APP_VERSION = constants.APP_VERSION # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # The Quart app: app = Quart(__name__) app = cors(app) # --- app.register_blueprint(in_out_report_bp, url_prefix = f"/{MODULE_BASE}/reports/in-out") app.register_blueprint(muster_roll_report_bp, url_prefix = f"/{MODULE_BASE}/reports/muster-roll") # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** @app.before_serving @set_api_version(api_version = APP_VERSION) # @log_request_to_mongo( # attr_name = "logs_mongo", # project = constants.PROJECT_NAME, # log_type = constants.MODULE_NAME, # operation = "apiStart", # log_input = True, # log_output = True # ) async def app_startup(**kwargs): """ To initialize the variables that you would like to use in this module. WARNING: ALL VARIABLES WILL BE INITIALIZED 'n' NUMBER OF TIMES, WHERE 'n' IS THE COUNT OF WORKERS DEPLOYED. SO, IF YOU WANT TO CONNECT TO A DATABASE AND YOU ALLOW A POOL-SIZE OF 10 AND IF YOU DEPLOY 4 WORKERS, YOU WILL END UP WITH 40 CONNECTIONS TO THE DATABASE. :return: None. """ # Safe-halt mechanism for upgrades (for a single-worker run): current_app.is_under_maintenance = False # ┳┓ ┓ • # ┃┃┏┓┣┓┓┏┏┓┏┓┓┏┓┏┓ # ┻┛┗ ┗┛┗┻┗┫┗┫┗┛┗┗┫ # ┛ ┛ ┛ # Debugging: enable_debugging = True if os.environ["DEBUG"].strip().lower() == "true" else False current_app.printer = IceCreamDebugger(prefix = f"{MODULE_BASE} (Q) | ", includeContext = True) if not enable_debugging: current_app.printer.disable() current_app.printer("initializing worker.") # ┓┏┏┳┓┏┳┓┏┓ ┏┓┓• # ┣┫ ┃ ┃ ┃┃ ┃ ┃┓┏┓┏┓╋ # ┛┗ ┻ ┻ ┣┛ ┗┛┗┗┗ ┛┗┗ # # Make an instance of an HTTP client to use to make API calls: # current_app.http_client = httpx.AsyncClient( # limits = httpx.Limits( # max_connections = 100, # ............ Maximum number of connections allowed in the pool. # max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive. # ), # timeout = httpx.Timeout( # pool = 120.0, # .... Time to wait for a free connection from the pool. # connect = 2.5, # ... Time to wait for establishing a connection to the server. # write = 10.0, # .... Time to wait for sending data. # read = 9.9 # ....... Time to wait for receiving data. # ) # ) # current_app.printer("HTTP client ready.") # ┏┓ ┓ ┓ ┳┓ # ┃ ┏┓┏┓┏┫ ┏┓┏┓┏┫ ┃┃┏┓╋┏┓ # ┗┛┛ ┗ ┗┻ ┗┻┛┗┗┻ ┻┛┗┻┗┗┻ # # Get the script credentials and data: # script_id = os.environ["SCRIPT_ID"] # response = await current_app.http_client.get( # url = r"https://nexcom.ditscentre.in/internal/cred/get", # headers = {"X-Script-Id": script_id} # ) # script_cred = response.json().get("data") # response = await current_app.http_client.get( # url = r"https://nexcom.ditscentre.in/internal/data/get", # headers = {"X-Script-Id": script_id} # ) # current_app.script_data = response.json().get("data") # current_app.printer("Cred and data ready.", type(script_cred), type(current_app.script_data)) # ┏┓ # ┗┓┓┏┏╋┏┓┏┳┓ # ┗┛┗┫┛┗┗ ┛┗┗ # ┛ pass # ┏┓ ┓ # ┃ ┏┓┏┣┓┏┓ # ┗┛┗┻┗┛┗┗ pass # ┳┳┓ ┳┓┳┓ # ┃┃┃┏┓┏┓┏┓┏┓┃┃┣┫ # ┛ ┗┗┛┛┗┗┫┗┛┻┛┻┛ # ┛ pass # ┏┓ # ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ # ┗┛┗┛┛┗┛┗┗ ┗┗┗┛┛ ┛ pass # ┏┓ ┓┓ # ┃ ┏┓┏┓╋┏┓┏┓┃┃┏┓┏┓┏ # ┗┛┗┛┛┗┗┛ ┗┛┗┗┗ ┛ ┛ pass # ┳┳┓• # ┃┃┃┓┏┏ # ┛ ┗┗┛┗ # Semaphores: current_app.chrome_semaphore = asyncio.Semaphore(1) # Helpers: muster_roll.init() # ┏┓┓ # ┃ ┃┏┓┏┓┏┓┓┏┏┓ # ┗┛┗┗ ┗┻┛┗┗┻┣┛ # ┛ # Remove unwanted/sensitive variables from RAM: # del script_cred gc.collect() # Done here! current_app.printer("Worker ready!") # --------------------------------------------------------------------------------------------------------------------- @app.after_serving @set_api_version(api_version = APP_VERSION) # @log_request_to_mongo( # attr_name = "logs_mongo", # project = constants.PROJECT_NAME, # log_type = constants.MODULE_NAME, # operation = "apiStop", # log_input = True, # log_output = True # ) async def app_shutdown(**kwargs): """ This is called when "app.shutdown()" is called. :return: None. """ current_app.printer("Shutting down...") # --------------------------------------------------------------------------------------------------------------------- @app.route(f"/", methods = ["GET", "POST"]) @app.route(f"/{MODULE_BASE}", methods = ["GET", "POST"]) async def root(): """ To check if the service is running or not. Use this to monitor the service from your "watchman" script. :return: only "ok" """ return "ok" # --------------------------------------------------------------------------------------------------------------------- @app.route(f"/metrics/memory", methods = ["GET", "POST"]) @app.route(f"/{MODULE_BASE}/metrics/memory", methods = ["GET", "POST"]) async def memory_metrics(): """ TO measure the metrics of the app. :return: A JSON of the metrics. """ # Figure pout the parent process. # This is important for multi-worker environments: parent_pid = os.getppid() parent_process = psutil.Process(parent_pid) parent_mem_info = parent_process.memory_info() # Figure out all the children of the parent process: child_pids = [child.pid for child in parent_process.children()] child_processes = [psutil.Process(child_pid) for child_pid in child_pids] child_stats = [] for pid, process in zip(child_pids, child_processes): mem_info = process.memory_info() child_stats.append({ "pid": pid, "mem": round(mem_info.rss / (1024 ** 2), 2) }) # Construct the response: metrics_json = { "project": constants.PROJECT_NAME, "parent": { "pid": parent_pid, "mem": round(parent_mem_info.rss / (1024 ** 2), 2) }, "children": child_stats, "unit": { "mem": "MB" }, "ts": date_time.get_current_ist_date_time(as_string = True) } # Done here: return metrics_json # --------------------------------------------------------------------------------------------------------------------- @app.route(f"/{MODULE_BASE}/debug/", methods = ["POST", "GET"]) async def change_debug(action): """ Enable or disable debugging for the entire microservice. WARNING: NOT RECOMMENDED FOR MULTI-WORKER DEPLOYMENTS. :param action: "enable" to allow debugging on the terminal, or "disable". :return: "enabled"/"disabled" if successful, else "ok" """ # Enable or disable debugging only if the password matches: action = action.lower() if action == "enable": current_app.printer.enable() elif action == "disable": current_app.printer.disable() return "ok" # --------------------------------------------------------------------------------------------------------------------- @app.route(f"/{MODULE_BASE}/maintenance/", methods = ["POST", "GET"]) async def change_maintenance(action): """ Enable or disable debugging for the entire microservice. WARNING: NOT RECOMMENDED FOR MULTI-WORKER DEPLOYMENTS. :param action: "enable" to stop taking new requests on the API, or "disable". :return: "enabled"/"disabled" if successful, else "ok" """ # Enable or disable debugging only if the password matches: action = action.lower() if action == "enable": current_app.is_under_maintenance = True os.environ["IS_UNDER_MAINTENANCE"] = "True" elif action == "disable": current_app.is_under_maintenance = False os.environ["IS_UNDER_MAINTENANCE"] = "False" return "ok" # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": # To get args from the terminal: import argparse # To run the ASGI: import uvicorn from multiprocessing import freeze_support # Get the config from the command-line: parser = argparse.ArgumentParser(description = f"Microservice for '{MODULE_BASE}' API.") parser.add_argument( "--workers", type = int, help = "The no. of threads to spin up for this instance!", default = 2 ) parser.add_argument( "--host", type = str, help = "The host for the app. e.g.: '0.0.0.0' or '127.0.0.1'.", default = "127.0.0.1" ) parser.add_argument( "--port", type = int, help = "The port no. to bind the app to.", default = 8080 ) parser.add_argument( "--script-id", type = str, help = "The id of this script (will affect the loaded config)." ) parser.add_argument( "--debug", action = "store_true", help = "Whether, or not, you want to see debugging messages in the terminal.", default = False ) args = parser.parse_args() # Note down the config; os.environ["SCRIPT_ID"] = args.script_id os.environ["DEBUG"] = str(args.debug) # Run the gateway: freeze_support() uvicorn.run( app = "main:app", workers = args.workers, host = args.host, port = args.port )