diff --git a/src/c3nav/mapdata/newapi/base.py b/src/c3nav/mapdata/newapi/base.py new file mode 100644 index 00000000..6fed8428 --- /dev/null +++ b/src/c3nav/mapdata/newapi/base.py @@ -0,0 +1,90 @@ +import json +from functools import wraps + +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.cache import get_conditional_response +from django.utils.http import quote_etag +from django.utils.translation import get_language +from ninja.decorators import decorate_view + +from c3nav.mapdata.api import api_stats_clean_location_value +from c3nav.mapdata.models import MapUpdate +from c3nav.mapdata.models.access import AccessPermission +from c3nav.mapdata.models.geometry.base import GeometryMixin +from c3nav.mapdata.utils.cache.local import LocalCacheProxy +from c3nav.mapdata.utils.cache.stats import increment_cache_key + +request_cache = LocalCacheProxy(maxsize=64) + + +def newapi_etag(permissions=True, etag_func=AccessPermission.etag_func, base_mapdata=False): + + def outer_wrapper(func): + @wraps(func) + def outer_wrapped_func(request, *args, **kwargs): + response = func(request, *args, **kwargs) + if response.status_code == 200: + response['ETag'] = request._target_etag + response['Cache-Control'] = 'no-cache' + request_cache.set(request._target_cache_key, response, 900) + return response + return outer_wrapped_func + + def inner_wrapper(func): + @wraps(func) + def inner_wrapped_func(request, *args, **kwargs): + # calculate the ETag + response_format = "json" + raw_etag = '%s:%s:%s' % (response_format, get_language(), + (etag_func(request) if permissions else MapUpdate.current_cache_key())) + if base_mapdata: + raw_etag += ':%d' % request.user_permissions.can_access_base_mapdata + etag = quote_etag(raw_etag) + + response = get_conditional_response(request, etag) + if response: + return response + + request._target_etag = etag + + # calculate the cache key + data = {} + for name, value in kwargs.items(): + try: + model_dump = value.model_dump + except AttributeError: + pass + else: + value = model_dump() + data[name] = value + cache_key = 'mapdata:api:%s:%s:%s' % ( + request.resolver_match.route.replace('/', '-').strip('-'), + raw_etag, + json.dumps(data, separators=(',', ':'), sort_keys=True, cls=DjangoJSONEncoder), + ) + + request._target_cache_key = cache_key + + response = request_cache.get(cache_key) + if response is not None: + return response + + with GeometryMixin.dont_keep_originals(): + return func(request, *args, **kwargs) + + return decorate_view(outer_wrapper)(inner_wrapped_func) + return inner_wrapper + + +def newapi_stats(stat_name): + def wrapper(func): + @wraps(func) + def wrapped_func(request, *args, **kwargs): + response = func(request, *args, **kwargs) + if response.status_code < 400 and kwargs: + name, value = next(iter(kwargs.items())) + for value in api_stats_clean_location_value(value): + increment_cache_key('apistats__%s__%s__%s' % (stat_name, name, value)) + return response + return wrapped_func + return decorate_view(wrapper)