(20241008) logging decorator and model improved.

This commit is contained in:
2024-10-08 15:36:20 +05:30
parent 1880840ae8
commit d9117f2a92
33 changed files with 1406 additions and 226 deletions
View File
View File
+406
View File
@@ -0,0 +1,406 @@
"""
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("/<json_type>/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("/<json_type>/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("/<json_type>/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("/<json_type>/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
View File
+381
View File
@@ -0,0 +1,381 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Created: Wednesday, 28th Aug., 2024
Updated: Tuesday, 8th Oct. 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 be used interchangeably, 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.string import json
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.response import ResponseModel
from utils_v2.api.async_quart import (
set_api_version,
read_input,
log_request_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
)
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
cred_and_data_bp = Blueprint("int_cnd", __name__)
# 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("/<json_type>/set", methods = ["POST", "GET"])
@set_api_version(api_version = "2.1.0")
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
project = "internal",
log_type = "credData",
operation = "set",
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", "X-Script-Desc"])
@handle_cancelled_request()
async def set_data(
json_type: str = None,
inbound_headers: dict = None,
inbound_data: dict = None,
inbound_files: dict = None,
**kwargs
):
"""
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. Ideally use this for only the first time setup.
:param json_type: The choice from one of the fields of 'JSON_TYPE_INFO'.
:param inbound_headers: auto-extracted by the decorators.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# If an invalid choice was made:
if json_type not in JSON_TYPE_INFO.keys():
return ResponseModel(
status_code = StatusCodes.FAILED,
message = f"invalid path '{json_type}'",
http_code = HttpCodes.BAD_REQUEST
)
# Construct the document:
description = inbound_headers.get("X-Script-Desc", "")
if len(description) > 200: description = description[:200]
document = {
"scriptId": inbound_headers["X-Script-Id"],
"desc": description,
"content": inbound_data
}
# 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 = document,
upsert = True,
raise_exception = True
)
# Return the response:
if success: return ResponseModel(status_code = StatusCodes.OK)
else: return ResponseModel(status_code = StatusCodes.FAILED)
# ---------------------------------------------------------------------------------------------------------------------
@cred_and_data_bp.route("/<json_type>/get", methods = ["POST", "GET"])
@set_api_version(api_version = "2.1.0")
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
project = "internal",
log_type = "credData",
operation = "get",
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"])
@handle_cancelled_request()
async def get_data(
json_type: str = None,
inbound_headers: dict = None,
inbound_data: dict = None,
inbound_files: dict = None,
**kwargs
):
"""
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.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# If an invalid choice was made:
if json_type not in JSON_TYPE_INFO.keys():
return ResponseModel(
status_code = StatusCodes.FAILED,
message = f"invalid path '{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},
raise_exception = True
)
# In case no result was found:
if cred_json is None:
return ResponseModel(
status_code = StatusCodes.FAILED,
message = "invalid script id"
)
# Successfully retrieved:
return ResponseModel(
status_code = StatusCodes.OK,
data = cred_json["content"]
)
# ---------------------------------------------------------------------------------------------------------------------
@cred_and_data_bp.route("/<json_type>/update", methods = ["POST", "GET"])
@set_api_version(api_version = "2.1.0")
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
project = "internal",
log_type = "credData",
operation = "update",
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"])
@handle_cancelled_request()
async def update_cred(
json_type: str = None,
inbound_headers: dict = None,
inbound_data: dict = None,
inbound_files: dict = None,
**kwargs
):
"""
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.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# If an invalid choice was made:
if json_type not in JSON_TYPE_INFO.keys():
return ResponseModel(
status_code = StatusCodes.FAILED,
message = f"invalid path '{json_type}'",
http_code = HttpCodes.BAD_REQUEST
)
# Pre-process the inbound data:
inbound_data = inbound_data or {}
# Prepare the update JSON. Pre-process the fields to set and unset.
# Our actual data/cred are held inside a field called "content", so we must wrap the request in that:
update_json = {}
if inbound_data.get("unset"):
update_json["$unset"] = current_app.mongo.dict_to_dot_notation({"content": inbound_data["unset"]})
if inbound_data.get("set"):
update_json["$set"] = current_app.mongo.dict_to_dot_notation({"content": 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,
raise_exception = True
)
# Return the response:
if success: return ResponseModel(status_code = StatusCodes.OK)
else: return ResponseModel(status_code = StatusCodes.FAILED)
# ---------------------------------------------------------------------------------------------------------------------
@cred_and_data_bp.route("/<json_type>/delete", methods = ["POST", "GET"])
@set_api_version(api_version = "2.1.0")
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
project = "internal",
log_type = "credData",
operation = "delete",
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,
**kwargs
):
"""
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.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# If an invalid choice was made:
if json_type not in JSON_TYPE_INFO.keys():
return ResponseModel(
status_code = StatusCodes.FAILED,
message = f"invalid path '{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(status_code = StatusCodes.OK)
else: return ResponseModel(status_code = StatusCodes.FAILED)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
+301
View File
@@ -0,0 +1,301 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Wednesday, 28th Aug., 2024
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(".")
sys.path.append("..")
# For system level activities:
import gc
import os
# For using Quart:
from quart import Quart, request, current_app
from quart_cors import cors
# To make REST-API calls:
import httpx
# My utils:
from utils_v2.string import json
from utils_v2.api import async_quart
from utils_v2.database.async_mongo_v2 import AsyncMongo
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 debugging:
from icecream import IceCreamDebugger
# Other blueprints:
from api.llm.chat_completion import llm_chat_bp
# LangChain-related:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Quart related:
MODULE_BASE = "ai"
APP_VERSION = "1.0.0"
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# The Quart app:
app = Quart(__name__)
app = cors(app)
app.register_blueprint(llm_chat_bp, url_prefix = f"/{MODULE_BASE}/llm")
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@app.before_serving
@log_request_to_mongo(
attr_name = "mongo",
log_type = MODULE_BASE,
operation = "apiStart",
api_version = APP_VERSION,
log_input = True,
log_output = True
)
async def app_startup():
"""
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:
current_app.printer = IceCreamDebugger(prefix = f"{MODULE_BASE} (Q) | ", includeContext = True)
if os.environ["DEBUG"] == "True": current_app.printer.disable()
# To make API calls:
max_connections = 5
limits = httpx.Limits(
max_keepalive_connections = max_connections,
max_connections = max_connections,
keepalive_expiry = 3600
)
current_app.http_client = httpx.AsyncClient(limits = limits)
# Get the credentials and data for this script:
script_id = os.environ.get("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")
# To connect to Mongo:
current_app.mongo = AsyncMongo(
connection_string = script_cred["mongoDb"]["dataDb"]["connectionString"],
database_name = script_cred["mongoDb"]["dataDb"]["dbName"],
max_connections = script_cred["mongoDb"]["dataDb"]["poolSize"],
debug = True if os.environ["DEBUG"] == "True" else False
)
# Remove unwanted/sensitive variables from RAM:
del script_cred
gc.collect()
# ---------------------------------------------------------------------------------------------------------------------
@app.after_serving
@log_request_to_mongo(
attr_name = "mongo",
log_type = MODULE_BASE,
operation = "apiStop",
api_version = APP_VERSION,
log_input = True,
log_output = True
)
async def app_shutdown():
"""
This is called when "app.shutdown()" is called.
:return: None.
"""
message = "Shutting down..."
current_app.printer(message)
# ---------------------------------------------------------------------------------------------------------------------
@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"/{MODULE_BASE}/debug/<action>", 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/<action>", 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
elif action == "disable": current_app.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(
"--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 = 8080
)