Squashed 'utils_v2/' changes from ef9630d..271426c

271426c (20241010) Can now cache class methods by accessing a caching object from the class itself!
606b02a Merge commit 'b0e0760fc7bb6336debf621860d1286575a0b8cc'
3d2a723 (20241009) Work in progress.
58266e3 (20241009) 'last decorator' bug-fixed
53e7a39 (20241009) Work in progress.
dbfc17f (20241009) 'last decorator' bug-fixed
11875e6 (20241008) logging decorator and model improved.

git-subtree-dir: utils_v2
git-subtree-split: 271426c19bea8de5edec7ecb79ee04368c47b02f
This commit is contained in:
2024-10-10 11:44:01 +05:30
parent 8494a2c0c0
commit 0450b2a445
17 changed files with 218 additions and 257 deletions
+47 -96
View File
@@ -46,13 +46,8 @@ from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.security import sanitizers
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.log import APILogModel
from utils_v2.api.response import ResponseModel
from utils_v2.api.metrics_prometheus import (
TOTAL_REQUEST_COUNT,
LIVE_REQUEST_COUNT,
REQUEST_LATENCY,
MetricsAPI
)
# To work with date and time:
import time
@@ -60,11 +55,9 @@ import datetime
# System-level activities:
import io
import distro
import socket
import platform
import os
# For Pydantic data-models:
# For Pydantic data-behaviour_models:
import pydantic
# For hashing and shortening the hash:
@@ -90,11 +83,6 @@ import asyncio
# *****************************************************************************************************************
# Info for logging that will stay constant during runtime:
SERVER_HOSTNAME = socket.gethostname()
PLATFORM_INFO = platform.uname()
HOST_OS = distro.name(True)
# Chars to choose from for random strings:
ALPHANUMERIC_CHARS = string.ascii_letters + string.digits
@@ -359,7 +347,7 @@ def set_api_version(api_version):
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -417,7 +405,7 @@ def read_input(
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -556,7 +544,7 @@ def validate_input(
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -572,6 +560,8 @@ def validate_input(
def log_request_to_mongo(
attr_name,
collection: str = "logs",
api_version: str = None,
project: str = None,
log_type: str = None,
operation: str = None,
log_input: bool = True,
@@ -586,6 +576,8 @@ def log_request_to_mongo(
:param attr_name: The name of the variable that holds the instance of 'AsyncMongo'. It should be accessible in the
scope of 'current_app'.
:param collection: The name of the collection to write the log into.
:param api_version: The version code of the API endpoint that is being logged.
:param project: The name of the project that the endpoint was built for.
:param log_type: A hint to identify what the log was for.
:param operation: A hint to identify what was action was being performed.
:param log_input: Whether, or not, you would like to log the input that came in.
@@ -602,6 +594,9 @@ def log_request_to_mongo(
# Let the next in-line decorator know that it has been wrapped:
kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
# Set the api version as needed:
kwargs["api_version"] = kwargs.get("api_version", api_version)
# Make variables and extract available info.:
exception = None
response = None
@@ -635,7 +630,8 @@ def log_request_to_mongo(
elif isinstance(response, tuple): response_to_log, http_code_to_log = response
else: response_to_log, http_code_to_log = str(response), 200
# Try to get the information about the request:
# Try to get the information about the request.
# There will be no data in any of these if the decorator was used to catch start-up and shut-down events.
request_method = None
request_url = None
request_route = None
@@ -653,46 +649,42 @@ def log_request_to_mongo(
except: pass
# Construct the log:
# for k in sensitive_keys: kwargs.get("inbound_headers", {}).pop(k, None)
# for k in sensitive_keys: kwargs.get("inbound_data", {}).pop(k, None)
log_json = {
"hostname": SERVER_HOSTNAME,
"os": f"{HOST_OS}",
"cpu": f"{PLATFORM_INFO.processor} ({PLATFORM_INFO.machine})",
"logId": kwargs.get("log_id"),
"logChain": kwargs.get("inbound_headers", {}).get("X-Log-Chain"),
"log": log_type,
"operation": operation,
"apiVer": kwargs.get("api_version"),
"method": request_method,
"url": request_url,
"route": request_route,
"ts": request_ts,
"tat": time.perf_counter() - start_ts,
"cpuTime": time.process_time() - cpu_start_ts,
"headers": kwargs.get("inbound_headers"),
"data": kwargs.get("inbound_data") if log_input else "not logged",
"files": {
api_log = APILogModel(
project = project,
log = log_type,
operation = operation,
apiVer = kwargs.get("api_version"),
logId = kwargs.get("log_id"),
logChain = kwargs.get("inbound_headers", {}).get("X-Log-Chain"),
method = request_method,
url = request_url,
route = request_route,
ts = request_ts,
tat = time.perf_counter() - start_ts,
cpuTime = time.process_time() - cpu_start_ts,
headers = kwargs.get("inbound_headers"),
data = kwargs.get("inbound_data") if log_input else "not logged",
files = {
k: {
"name": v["name"],
"size": v["size"]
} for k, v in kwargs.get("inbound_files", {}).items()
},
"exception": None if exception is None else describe_exception(exception),
"response": response_to_log,
"httpCode": http_code_to_log
}
exception = None if exception is None else describe_exception(exception),
response = response_to_log,
httpCode = http_code_to_log
)
# Write the log:
app_attr = getattr(current_app, attr_name)
inserted_id = await app_attr.insert_one(
collection = collection,
document = log_json
document = api_log.model_dump()
)
# Return the response from the wrapped function.
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -709,7 +701,9 @@ def should_not_be_under_maintenance(attr_name):
"""
Use this decorator to reject a request when the app is being marked as "under-maintenance". You will need to create
a boolean variable within the scope of the 'current_app' for this to work.
a boolean variable within the scope of the 'current_app' for this to work. An alternate to this is to set the value
in an environment variable named 'IS_UNDER_MAINTENANCE' to a string value of either 'True' or 'False' for
multi-worker deployments.
:param attr_name: The name of the boolean variable that will hold the information about the app being under
maintenance. If its value is True at the time of checking, the incoming request will be rejected.
:return: The decorator factory.
@@ -726,14 +720,16 @@ def should_not_be_under_maintenance(attr_name):
# Get the attribute and check if it indicates that the app is under maintenance,
# call the wrapped function if not under maintenance:
app_attr = getattr(current_app, attr_name)
if app_attr: response = ResponseModel(status_code = StatusCodes.DOWN_FOR_MAINTENANCE).for_quart()
env_attr = True if os.environ.get("IS_UNDER_MAINTENANCE", "False").lower() == "true" else False
if app_attr or env_attr:
response = ResponseModel(status_code = StatusCodes.DOWN_FOR_MAINTENANCE).for_quart()
else:
response = await func(*args, **kwargs)
kwargs["decorator_count"] -= 1
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -780,7 +776,7 @@ def only_whitelisted_ips(attr_name):
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -888,7 +884,7 @@ def limit_rate(
# Return the response from the function call:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -927,7 +923,7 @@ def handle_cancelled_request(cleanup_func = None, cleanup_coro = None):
# Done here:
if (
kwargs["decorator_count"] == 1 and
kwargs["decorator_count"] == 0 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
@@ -950,51 +946,6 @@ def handle_cancelled_request(cleanup_func = None, cleanup_coro = None):
return decorator
# ---------------------------------------------------------------------------------------------------------------------
def measure_metrics_for_prometheus():
"""
Use this decorator to automatically measure metrics for using in Prometheus.
:return: The decorator factory.
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Let the next in-line decorator know that it has been wrapped:
kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
# We use the context of the measurement class:
async with MetricsAPI(
method = f"{request.method}",
endpoint = str(request.url_rule.rule)
) as metrics:
# Invoke the wrapped function:
response = await func(*args, **kwargs)
kwargs["decorator_count"] -= 1
# Interpret the HTTP code:
if isinstance(response, ResponseModel): _, metrics.http_code = response.for_quart()
elif isinstance(response, tuple): metrics.http_code = response[1]
else: metrics.http_code = 200
# Done here:
if (
kwargs["decorator_count"] == 1 and
isinstance(response, ResponseModel)
): response = response.for_quart()
return response
return wrapper
return decorator
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***