init
This commit is contained in:
294
backend-django/backend/utils/decorators.py
Normal file
294
backend-django/backend/utils/decorators.py
Normal file
@@ -0,0 +1,294 @@
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
from logging import Logger, LoggerAdapter
|
||||
from types import (BuiltinFunctionType, BuiltinMethodType,
|
||||
ClassMethodDescriptorType, FunctionType,
|
||||
MethodDescriptorType, MethodType)
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from django.db import connections
|
||||
|
||||
from .logging import TAG_DATA_FEED, TAG_PERF, get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# attribut de classe : logger
|
||||
CLASS_ATTR_LOGGER = 'logger'
|
||||
|
||||
# clé du dictionnaire contexte : désactivé
|
||||
CTX_KEY_DISABLED = 'disabled'
|
||||
|
||||
# clé du dictionnaire contexte : logger
|
||||
CTX_KEY_LOGGER = 'logger'
|
||||
|
||||
# attribut de décorateur : contexte
|
||||
DECORATOR_ATTR_CTX = 'decorator_ctx'
|
||||
|
||||
# attribut de décorateur : clé
|
||||
DECORATOR_ATTR_KEY = 'decorator_key'
|
||||
|
||||
# attribut de décorateur : cible (classe, fonction)
|
||||
DECORATOR_ATTR_TARGET = 'decorator_target'
|
||||
|
||||
# types de fonctions
|
||||
FUNCTION_TYPES = (
|
||||
BuiltinFunctionType,
|
||||
BuiltinMethodType,
|
||||
ClassMethodDescriptorType,
|
||||
FunctionType,
|
||||
MethodDescriptorType,
|
||||
MethodType,
|
||||
)
|
||||
|
||||
|
||||
def _get_query_count() -> int:
|
||||
""" renvoie le nombre total de requêtes (il peut y avoir plusieurs connexions) """
|
||||
|
||||
return sum(len(c.queries) for c in connections.all())
|
||||
|
||||
|
||||
class OnConflict(Enum):
|
||||
""" choix de gestion de conflit quand le même décorateur est déjà utilisé """
|
||||
|
||||
# laisse l'ancien décorateur
|
||||
SKIP = auto()
|
||||
|
||||
# remplace l'ancien décorateur
|
||||
REPLACE = auto()
|
||||
|
||||
# ajoute à l'ancien décorateur
|
||||
STACK = auto()
|
||||
|
||||
|
||||
# amélioration possible : et s'il y a plusieurs décorateurs de même clé ?
|
||||
def _find_former_decorator(key: str, target: Callable) -> Tuple[Optional[Callable], Optional[Callable]]:
|
||||
"""
|
||||
Trouve le décorateur de même clé si la cible est déjà décorée. Un décorateur désactivé est ignoré.
|
||||
note : ne fonctionne pas correctement s'il existe un décorateur intermédiaire sans attribut DECORATOR_ATTR_TARGET
|
||||
|
||||
:param key: clé de décorateur
|
||||
:type key: str
|
||||
|
||||
:param target: fonction potentiellement décorée
|
||||
:type target: Callable
|
||||
|
||||
:return: ancien décorateur + ancienne cible
|
||||
:rtype: Tuple[Callable, Callable]
|
||||
"""
|
||||
current = target
|
||||
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
||||
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
||||
|
||||
while decorator_key and decorator_key != key and decorator_target:
|
||||
current = decorator_target
|
||||
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
||||
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
||||
|
||||
# les décorateurs désactivés ne comptent pas
|
||||
if decorator_key == key and not getattr(current, DECORATOR_ATTR_CTX, {}).get(CTX_KEY_DISABLED, False):
|
||||
return current, decorator_target
|
||||
return None, None
|
||||
|
||||
|
||||
def _create_decorator_from_action(
|
||||
action: Callable,
|
||||
decorator_key: str,
|
||||
on_conflict: OnConflict = OnConflict.SKIP):
|
||||
"""
|
||||
Fonction d'ordre supérieur qui construit un décorateur de fonction/méthode à partir des paramètres. Ce décorateur exécute l'action fournie.
|
||||
|
||||
Un contexte (dictionnaire) est passé à l'appel. Il contient :
|
||||
- l'état désactivé : clé CTX_KEY_DISABLED optionnelle
|
||||
|
||||
:param action: action à exécuter
|
||||
:type action: Callable[[dict, Callable[P, R], P.args, P.kwargs], R]
|
||||
|
||||
:param decorator_key: clé correspondant au décorateur, permet de gérer les conflits
|
||||
:type decorator_key: str, optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
"""
|
||||
def inner(func):
|
||||
if on_conflict is not OnConflict.STACK:
|
||||
former_decorator, former_target = _find_former_decorator(decorator_key, func)
|
||||
if former_decorator:
|
||||
if on_conflict is OnConflict.SKIP:
|
||||
# pas de nouveau décorateur
|
||||
return func
|
||||
|
||||
if on_conflict is OnConflict.REPLACE:
|
||||
if former_decorator is func and former_target:
|
||||
# ancien décorateur déjà fourni : on le remplace vraiment
|
||||
func = former_target
|
||||
else:
|
||||
# ancien décorateur déjà décoré : on le désactive
|
||||
former_ctx = getattr(former_decorator, DECORATOR_ATTR_CTX, None)
|
||||
if former_ctx is None:
|
||||
former_ctx = {}
|
||||
getattr(former_decorator, DECORATOR_ATTR_CTX, former_ctx)
|
||||
former_ctx.update({CTX_KEY_DISABLED: True})
|
||||
ctx = {}
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if ctx.get(CTX_KEY_DISABLED, False):
|
||||
# désactivé : simple appel de la fonction d'origine
|
||||
return func(*args, **kwargs)
|
||||
return action(ctx, func, *args, **kwargs)
|
||||
|
||||
setattr(wrapper, DECORATOR_ATTR_CTX, ctx)
|
||||
setattr(wrapper, DECORATOR_ATTR_KEY, decorator_key)
|
||||
setattr(wrapper, DECORATOR_ATTR_TARGET, func)
|
||||
return wrapper
|
||||
return inner
|
||||
|
||||
|
||||
def execution_time(
|
||||
level: int = logging.DEBUG,
|
||||
logger_name: Any = None,
|
||||
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
||||
on_conflict: OnConflict = OnConflict.SKIP,
|
||||
warn_after: int = None):
|
||||
"""
|
||||
Décorateur de fonction pour tracer le temps d'exécution (au niveau <level> avec un logger de performances).
|
||||
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
||||
|
||||
:param level: niveau de base du logger, defaults to logging.DEBUG
|
||||
:type level: int
|
||||
|
||||
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
||||
:type logger_name: str, optional
|
||||
|
||||
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
||||
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
|
||||
:param warn_after: durée (en ms) au-delà de laquelle on souhaite un log d'avertissement, defaults to None
|
||||
:type warn_after: int, optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
|
||||
def action(ctx: Dict, func, *args, **kwargs):
|
||||
if CTX_KEY_LOGGER not in ctx:
|
||||
# initialisation du logger, pas toujours possible avant le premier appel
|
||||
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
||||
|
||||
temps_debut = time.time()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
temps_ecoule = round((time.time() - temps_debut) * 1000)
|
||||
log_level = logging.WARNING if isinstance(warn_after, int) and temps_ecoule > warn_after else level
|
||||
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' exécuté en %d ms", func.__name__, temps_ecoule)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer la durée')
|
||||
|
||||
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
||||
|
||||
|
||||
def query_count(
|
||||
level: int = logging.DEBUG,
|
||||
logger_name: Any = None,
|
||||
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
||||
on_conflict: OnConflict = OnConflict.SKIP,
|
||||
warn_after: int = None):
|
||||
"""
|
||||
Décorateur de fonction pour tracer le nombre de requêtes (au niveau <level> avec un logger de performances).
|
||||
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
||||
|
||||
:param level: niveau de base du logger, defaults to logging.DEBUG
|
||||
:type level: int
|
||||
|
||||
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
||||
:type logger_name: str, optional
|
||||
|
||||
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
||||
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
|
||||
:param warn_after: nombre de requêtes au-delà duquel on souhaite un log d'avertissement, defaults to None
|
||||
:type warn_after: int, optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
|
||||
def action(ctx: Dict, func, *args, **kwargs):
|
||||
if CTX_KEY_LOGGER not in ctx:
|
||||
# initialisation du logger, pas toujours possible avant le premier appel
|
||||
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
||||
|
||||
nb_debut = _get_query_count()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
nb_requetes = _get_query_count() - nb_debut
|
||||
log_level = logging.WARNING if isinstance(warn_after, int) and nb_requetes > warn_after else level
|
||||
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' a exécuté %d requête(s)", func.__name__, nb_requetes)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer le nombre de requêtes')
|
||||
|
||||
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
||||
|
||||
|
||||
def class_logger(cls):
|
||||
"""
|
||||
Décorateur de classe pour stocker un logger standard dans l'attribut 'logger' (aucune gestion de conflit)
|
||||
"""
|
||||
if not inspect.isclass(cls):
|
||||
return cls
|
||||
|
||||
@functools.wraps(cls, updated=())
|
||||
class Wrapper(cls):
|
||||
decorated = 1
|
||||
|
||||
setattr(Wrapper, CLASS_ATTR_LOGGER, get_logger(Wrapper))
|
||||
return Wrapper
|
||||
|
||||
|
||||
def decorate_functions(
|
||||
decorator: Callable,
|
||||
func_filter: Callable[[Callable], bool],
|
||||
factory: bool = False):
|
||||
"""
|
||||
Décorateur de classe qui applique le décorateur donné aux fonctions/méthodes (attributs d'un type de FUNCTION_TYPES) qui passent le filtre.
|
||||
|
||||
:param decorator: décorateur à appliquer
|
||||
:type decorator: Callable
|
||||
|
||||
:param func_filter: filtre permettant de sélectionner les fonctions/méthodes à décorer
|
||||
:type func_filter: Callable[[Callable], bool]
|
||||
|
||||
:param factory: True indique que le décorateur est une méthode "factory" à exécuter avec la classe, defaults to False
|
||||
:type factory: bool
|
||||
"""
|
||||
|
||||
def decorate(cls):
|
||||
if not inspect.isclass(cls):
|
||||
return cls
|
||||
|
||||
@functools.wraps(cls, updated=())
|
||||
class Wrapper(cls):
|
||||
decorated = 1
|
||||
|
||||
_decorator = decorator(Wrapper) if factory else decorator
|
||||
for attr_name in dir(Wrapper):
|
||||
# __new__ et __init__ sont complexes à décorer, à gérer au cas par cas
|
||||
if attr_name != '__new__' and attr_name != '__init__':
|
||||
value = getattr(Wrapper, attr_name)
|
||||
if isinstance(value, FUNCTION_TYPES) and func_filter(value):
|
||||
setattr(Wrapper, attr_name, _decorator(value))
|
||||
return Wrapper
|
||||
return decorate
|
||||
Reference in New Issue
Block a user