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:
+47
-96
@@ -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 ***
|
||||
|
||||
Reference in New Issue
Block a user