misc: move cache decorators in utils (#61735)
This commit is contained in:
parent
0423dbbbca
commit
c75d48967e
|
@ -26,7 +26,8 @@ from django.utils.text import slugify
|
|||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentic2.decorators import GlobalCache, errorcollector
|
||||
from authentic2.decorators import errorcollector
|
||||
from authentic2.utils.cache import GlobalCache
|
||||
from django_rbac import utils as rbac_utils
|
||||
from django_rbac.models import (
|
||||
VIEW_OP,
|
||||
|
|
|
@ -14,10 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
import re
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from json import dumps as json_dumps
|
||||
|
@ -26,16 +23,18 @@ from django.core.exceptions import ValidationError
|
|||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.views.debug import technical_404_response
|
||||
|
||||
from . import app_settings, middleware
|
||||
from . import app_settings
|
||||
from .utils.cache import ( # pylint: disable=unused-import
|
||||
CacheUnusable,
|
||||
GlobalCache,
|
||||
RequestCache,
|
||||
SessionCache,
|
||||
)
|
||||
|
||||
# XXX: import to_list for retrocompaibility
|
||||
from .utils.misc import to_iter, to_list # pylint: disable=unused-import
|
||||
|
||||
|
||||
class CacheUnusable(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def unless(test, message):
|
||||
'''Decorator returning a 404 status code if some condition is not met'''
|
||||
|
||||
|
@ -124,160 +123,6 @@ def _wrap_instance__resolve(wrapping_functions, instance):
|
|||
return instance
|
||||
|
||||
|
||||
class CacheDecoratorBase:
|
||||
"""Base class to build cache decorators.
|
||||
|
||||
It helps for building keys from function arguments.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if len(args) > 1:
|
||||
raise TypeError(
|
||||
'%s got unexpected arguments, only one argument must be given, the function to decorate'
|
||||
% cls.__name__
|
||||
)
|
||||
if args:
|
||||
# Case of a decorator used directly
|
||||
return cls(**kwargs)(args[0])
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, timeout=None, hostname_vary=True, args=None, kwargs=None):
|
||||
self.timeout = timeout
|
||||
self.hostname_vary = hostname_vary
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def set(self, key, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func)
|
||||
def f(*args, **kwargs):
|
||||
try:
|
||||
if not app_settings.A2_CACHE_ENABLED:
|
||||
raise CacheUnusable
|
||||
now = time.time()
|
||||
key = self.key(*args, **kwargs)
|
||||
value, tstamp = self.get(key)
|
||||
if tstamp is not None:
|
||||
if self.timeout is None or tstamp + self.timeout > now:
|
||||
return value
|
||||
if hasattr(self, 'delete'):
|
||||
self.delete(key, (key, tstamp))
|
||||
value = func(*args, **kwargs)
|
||||
self.set(key, (value, now))
|
||||
return value
|
||||
except CacheUnusable: # fallback when cache cannot be used
|
||||
return func(*args, **kwargs)
|
||||
|
||||
f.cache = self
|
||||
return f
|
||||
|
||||
def key(self, *args, **kwargs):
|
||||
'''Transform arguments to string and build a key from it'''
|
||||
parts = [str(id(self))] # add cache instance to the key
|
||||
if self.hostname_vary:
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
parts.append(request.get_host())
|
||||
else:
|
||||
# if we cannot determine the hostname it's better to ignore the
|
||||
# cache
|
||||
raise CacheUnusable
|
||||
for i, arg in enumerate(args):
|
||||
if self.args and i not in self.args:
|
||||
continue
|
||||
parts.append(str(arg))
|
||||
|
||||
for kw, arg in sorted(kwargs.items(), key=lambda x: x[0]):
|
||||
if kw not in self.kwargs:
|
||||
continue
|
||||
parts.append('%s-%s' % (str(kw), str(arg)))
|
||||
return '|'.join(parts)
|
||||
|
||||
|
||||
class SimpleDictionnaryCacheMixin:
|
||||
"""Default implementations of set, get and delete for a cache implemented
|
||||
using a dictionary. The dictionnary must be returned by a property named
|
||||
'cache'.
|
||||
"""
|
||||
|
||||
def set(self, key, value):
|
||||
self.cache[key] = value
|
||||
|
||||
def get(self, key):
|
||||
return self.cache.get(key, (None, None))
|
||||
|
||||
def delete(self, key, value):
|
||||
if key in self.cache and self.cache[key] == value:
|
||||
del self.cache[key]
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
|
||||
|
||||
class GlobalCache(SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cache = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestCache(SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
@property
|
||||
def cache(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if not request:
|
||||
return {}
|
||||
# create a cache dictionary on the request
|
||||
return request.__dict__.setdefault(self.__class__.__name__, {})
|
||||
|
||||
|
||||
class PickleCacheMixin:
|
||||
def set(self, key, value):
|
||||
value, tstamp = value
|
||||
value = base64.b64encode(pickle.dumps(value)).decode('ascii')
|
||||
super().set(key, (value, tstamp))
|
||||
|
||||
def get(self, key):
|
||||
value = super().get(key)
|
||||
if value[0] is not None:
|
||||
value, tstamp = value
|
||||
try:
|
||||
value = base64.b64decode(value.encode('ascii'))
|
||||
except ValueError:
|
||||
pass
|
||||
value = (pickle.loads(value), tstamp)
|
||||
return value
|
||||
|
||||
|
||||
class SessionCache(PickleCacheMixin, SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
@property
|
||||
def cache(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if not request:
|
||||
return {}
|
||||
# create a cache dictionary on the request
|
||||
return request.session.setdefault(self.__class__.__name__, {})
|
||||
|
||||
def set(self, key, value):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
request.session.modified = True
|
||||
return super().set(key, value)
|
||||
|
||||
def clear(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
request.session.modified = True
|
||||
return super().clear()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def errorcollector(error_dict):
|
||||
try:
|
||||
|
|
|
@ -20,9 +20,10 @@ from django.apps import apps
|
|||
from django.conf import settings
|
||||
|
||||
from . import decorators
|
||||
from .utils.cache import GlobalCache
|
||||
|
||||
|
||||
@decorators.GlobalCache
|
||||
@GlobalCache
|
||||
def get_hooks(hook_name):
|
||||
"""Return a list of defined hook named a2_hook<hook_name> on AppConfig classes of installed
|
||||
Django applications.
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit
|
||||
from authentic2.decorators import GlobalCache
|
||||
from authentic2.utils.cache import GlobalCache
|
||||
|
||||
|
||||
def label_from_user(user):
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from authentic2.decorators import GlobalCache
|
||||
from authentic2.utils.cache import GlobalCache
|
||||
|
||||
|
||||
@GlobalCache(timeout=60)
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from authentic2 import app_settings, middleware
|
||||
|
||||
|
||||
class CacheUnusable(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class CacheDecoratorBase:
|
||||
"""Base class to build cache decorators.
|
||||
|
||||
It helps for building keys from function arguments.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if len(args) > 1:
|
||||
raise TypeError(
|
||||
'%s got unexpected arguments, only one argument must be given, the function to decorate'
|
||||
% cls.__name__
|
||||
)
|
||||
if args:
|
||||
# Case of a decorator used directly
|
||||
return cls(**kwargs)(args[0])
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, timeout=None, hostname_vary=True, args=None, kwargs=None):
|
||||
self.timeout = timeout
|
||||
self.hostname_vary = hostname_vary
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def set(self, key, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func)
|
||||
def f(*args, **kwargs):
|
||||
try:
|
||||
if not app_settings.A2_CACHE_ENABLED:
|
||||
raise CacheUnusable
|
||||
now = time.time()
|
||||
key = self.key(*args, **kwargs)
|
||||
value, tstamp = self.get(key)
|
||||
if tstamp is not None:
|
||||
if self.timeout is None or tstamp + self.timeout > now:
|
||||
return value
|
||||
if hasattr(self, 'delete'):
|
||||
self.delete(key, (key, tstamp))
|
||||
value = func(*args, **kwargs)
|
||||
self.set(key, (value, now))
|
||||
return value
|
||||
except CacheUnusable: # fallback when cache cannot be used
|
||||
return func(*args, **kwargs)
|
||||
|
||||
f.cache = self
|
||||
return f
|
||||
|
||||
def key(self, *args, **kwargs):
|
||||
'''Transform arguments to string and build a key from it'''
|
||||
parts = [str(id(self))] # add cache instance to the key
|
||||
if self.hostname_vary:
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
parts.append(request.get_host())
|
||||
else:
|
||||
# if we cannot determine the hostname it's better to ignore the
|
||||
# cache
|
||||
raise CacheUnusable
|
||||
for i, arg in enumerate(args):
|
||||
if self.args and i not in self.args:
|
||||
continue
|
||||
parts.append(str(arg))
|
||||
|
||||
for kw, arg in sorted(kwargs.items(), key=lambda x: x[0]):
|
||||
if kw not in self.kwargs:
|
||||
continue
|
||||
parts.append('%s-%s' % (str(kw), str(arg)))
|
||||
return '|'.join(parts)
|
||||
|
||||
|
||||
class SimpleDictionnaryCacheMixin:
|
||||
"""Default implementations of set, get and delete for a cache implemented
|
||||
using a dictionary. The dictionnary must be returned by a property named
|
||||
'cache'.
|
||||
"""
|
||||
|
||||
def set(self, key, value):
|
||||
self.cache[key] = value
|
||||
|
||||
def get(self, key):
|
||||
return self.cache.get(key, (None, None))
|
||||
|
||||
def delete(self, key, value):
|
||||
if key in self.cache and self.cache[key] == value:
|
||||
del self.cache[key]
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
|
||||
|
||||
class GlobalCache(SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cache = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestCache(SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
@property
|
||||
def cache(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if not request:
|
||||
return {}
|
||||
# create a cache dictionary on the request
|
||||
return request.__dict__.setdefault(self.__class__.__name__, {})
|
||||
|
||||
|
||||
class PickleCacheMixin:
|
||||
def set(self, key, value):
|
||||
value, tstamp = value
|
||||
value = base64.b64encode(pickle.dumps(value)).decode('ascii')
|
||||
super().set(key, (value, tstamp))
|
||||
|
||||
def get(self, key):
|
||||
value = super().get(key)
|
||||
if value[0] is not None:
|
||||
value, tstamp = value
|
||||
try:
|
||||
value = base64.b64decode(value.encode('ascii'))
|
||||
except ValueError:
|
||||
pass
|
||||
value = (pickle.loads(value), tstamp)
|
||||
return value
|
||||
|
||||
|
||||
class SessionCache(PickleCacheMixin, SimpleDictionnaryCacheMixin, CacheDecoratorBase):
|
||||
@property
|
||||
def cache(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if not request:
|
||||
return {}
|
||||
# create a cache dictionary on the request
|
||||
return request.session.setdefault(self.__class__.__name__, {})
|
||||
|
||||
def set(self, key, value):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
request.session.modified = True
|
||||
return super().set(key, value)
|
||||
|
||||
def clear(self):
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
request.session.modified = True
|
||||
return super().clear()
|
|
@ -18,7 +18,7 @@ import urllib.parse
|
|||
|
||||
from django.apps import apps
|
||||
|
||||
from authentic2.decorators import GlobalCache
|
||||
from authentic2.utils.cache import GlobalCache
|
||||
from authentic2.utils.misc import same_origin
|
||||
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@ from jwcrypto.jwk import JWK
|
|||
from jwcrypto.jwt import JWT
|
||||
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.decorators import GlobalCache
|
||||
from authentic2.models import Attribute
|
||||
from authentic2.utils.cache import GlobalCache
|
||||
|
||||
from . import models
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@ class CacheTests(TestCase):
|
|||
def test_cache_decorator_base(self):
|
||||
import random
|
||||
|
||||
from authentic2.decorators import CacheDecoratorBase
|
||||
from authentic2.utils.cache import CacheDecoratorBase
|
||||
|
||||
class GlobalCache(CacheDecoratorBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
Loading…
Reference in New Issue