""" This module contains decorators for the API endpoints. """ import functools import hashlib import json from typing import Any, Awaitable, Callable from fastapi import Request from fastapi.responses import JSONResponse from monday_com_orca_backend.utils.cache import TTLCache # Cache for requests. Use compression for memory efficiency (the time # tradeoff is negligible because it's still faster than querying the database). # The TTL is 1h to have a good balance between freshness and performance. request_cache: TTLCache = TTLCache(maxsize=256, ttl=3600, compress=True) def cache_response( func: Callable[..., Awaitable[Any]], ) -> Callable[..., Awaitable[Any]]: """ Decorator to cache the response of a view function. Works for GET and POST requests. The cache key is generated based on the request path, query parameters, and request body. """ @functools.wraps(func) async def wrapper(*args, request: Request, **kwargs): # Generate cache key based on request path and query parameters full_path_no_params = request.url.path # Sort query parameters to ensure consistent cache keys regardless of order sorted_query_params = dict(sorted(request.query_params.items())) cache_key_parts = {"path": full_path_no_params, "query": sorted_query_params} # For POST requests, include the request body in the cache key if request.method == "POST": # Check if body was already read and stored if hasattr(request.state, "cached_body"): body = request.state.cached_body else: body = await request.body() # Store the body in request state so it can be read again request.state.cached_body = body # Try to normalize JSON body to ensure consistent ordering try: # Parse JSON and re-serialize with sorted keys for consistent hashing json_data = json.loads(body.decode("utf-8")) normalized_body = json.dumps( json_data, sort_keys=True, separators=(",", ":") ).encode("utf-8") body_hash = hashlib.md5(normalized_body).hexdigest() except (json.JSONDecodeError, UnicodeDecodeError): # If not valid JSON, use raw body hash body_hash = hashlib.md5(body).hexdigest() cache_key_parts["body"] = body_hash cache_key = _cache_key_factory("", cache_key_parts) cached_data = request_cache.get(cache_key) if cached_data is not None: return cached_data response = await func(*args, request=request, **kwargs) if not isinstance(response, JSONResponse): response = JSONResponse(content=response) request_cache[cache_key] = response return response return wrapper def _cache_key_factory(base_key: str, args: dict) -> str: """ Create a cache key based on the provided arguments. We use a tuple of the arguments to ensure uniqueness, so only unique combinations of arguments will create a new cache entry. """ # Sort the arguments to ensure consistent ordering args = {k: args[k] for k in sorted(args)} return f"{base_key}:{tuple(args.items())}"