From 8d4feb0cc715d51d87213d5f40b820c6f7e9a378 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 15 Aug 2025 12:15:04 +0300 Subject: [PATCH 1/7] add methods to CacheDropConfig --- fast_cache_middleware/depends.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fast_cache_middleware/depends.py b/fast_cache_middleware/depends.py index 9b4528a..88756f4 100644 --- a/fast_cache_middleware/depends.py +++ b/fast_cache_middleware/depends.py @@ -46,9 +46,14 @@ class CacheDropConfig(BaseCacheConfigDepends): that matches the beginning of request path. """ - def __init__(self, paths: list[str | re.Pattern]) -> None: + def __init__( + self, + paths: list[str | re.Pattern] | None = None, + methods: list[Callable] | None = None, + ) -> None: self.paths: list[re.Pattern] = [ - p if isinstance(p, re.Pattern) else re.compile(f"^{p}") for p in paths + p if isinstance(p, re.Pattern) else re.compile(f"^{p}") for p in paths or [] ] + self.methods: list[Callable] = methods or [] self.dependency = self From e3a2071639baaf0035f923bb1543e5cf0391eae1 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 15 Aug 2025 12:15:29 +0300 Subject: [PATCH 2/7] add methods for convert methods to path --- fast_cache_middleware/middleware.py | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index ac009c2..0a70262 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -1,5 +1,6 @@ import copy import logging +import re import typing as tp from fastapi import FastAPI, routing @@ -294,12 +295,16 @@ def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo routes: List of routes to analyze """ routes_info = [] + route_names = {route.name: route.path for route in routes} + for route in routes: ( cache_config, cache_drop_config, ) = self._extract_cache_configs_from_route(route) + self._convert_methods_to_path(route_names, cache_drop_config) + if cache_config or cache_drop_config: cache_configuration = CacheConfiguration( max_age=cache_config.max_age if cache_config else None, @@ -308,7 +313,11 @@ def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo cache_drop_config.paths if cache_drop_config else None ), ) - + logger.debug( + "Extracted cache configuration: %s for paths %s", + cache_configuration.invalidate_paths, + route.name, + ) route_info = RouteInfo( route=route, cache_config=cache_configuration, @@ -348,6 +357,33 @@ def _extract_cache_configs_from_route( return cache_config, cache_drop_config + def _convert_methods_to_path( + self, + route_names: dict[str, str], + cache_drop_config: CacheDropConfig, + ) -> list[re.Pattern] | None: + if not cache_drop_config: + return None + + for method in cache_drop_config.methods: + name = getattr(method, "__name__", None) + route = route_names.get(name) + if not route: + continue + + m = re.match(r"(/[^/]+)", route) + if not m: + continue + first_seg = m.group(1) + + pattern_path = rf"^{re.escape(first_seg)}(?:/|$)" + logger.debug( + "Route '%s' -> first segment pattern '%s'", route, pattern_path + ) + cache_drop_config.paths.append(re.compile(pattern_path)) + + return cache_drop_config.paths + def _find_matching_route( self, request: Request, routes_info: list[RouteInfo] ) -> tp.Optional[RouteInfo]: From 639e6d47fde7bf294bd3b6db739e3f92a6d67a42 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 15 Aug 2025 12:16:17 +0300 Subject: [PATCH 3/7] added more sophisticated methods to invalidate the cache --- examples/basic.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/basic.py b/examples/basic.py index d5929e3..68eb073 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -115,6 +115,16 @@ async def get_users() -> tp.List[UserResponse]: ] +@app.get("/orgs/{org_id}/users/{user_id}", dependencies=[CacheConfig(max_age=300)]) +async def get_user_in_org(org_id: int, user_id: int) -> UserResponse: + """Получение пользователя в конкретной организации. + + Пример более сложного пути с несколькими параметрами. + """ + user = _USERS_STORAGE.get(user_id) + return UserResponse(user_id=user_id, name=user.name, email=user.email) + + @app.post("/users/{user_id}", dependencies=[CacheDropConfig(paths=["/users"])]) async def create_user(user_id: int, user_data: User) -> UserResponse: """Создание пользователя с инвалидацией кеша. From 6f9df8e250bad629328bf0ab239bea8b3b3dad6c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 15 Aug 2025 12:18:07 +0300 Subject: [PATCH 4/7] edited endpoint deletion: cache invalidation using the method --- examples/basic.py | 5 ++++- tests/conftest.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/basic.py b/examples/basic.py index 68eb073..f7e5af6 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -147,7 +147,10 @@ async def update_user(user_id: int, user_data: User) -> UserResponse: return UserResponse(user_id=user_id, name=user_data.name, email=user_data.email) -@app.delete("/users/{user_id}", dependencies=[CacheDropConfig(paths=["/users"])]) +@app.delete( + "/users/{user_id}", + dependencies=[CacheDropConfig(methods=[get_user, get_user_in_org])], +) async def delete_user(user_id: int) -> UserResponse: """Удаление пользователя с инвалидацией кеша.""" user = _USERS_STORAGE.get(user_id) diff --git a/tests/conftest.py b/tests/conftest.py index 7be5a19..5bb9af7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,7 +85,7 @@ def app() -> FastAPI: app.router.add_api_route( "/users/{user_id}", delete_user, - dependencies=[CacheDropConfig(paths=["/users/"])], + dependencies=[CacheDropConfig(methods=[get_user])], methods={HTTPMethod.DELETE.value}, ) app.router.add_api_route( From 29cee9585024327c86c16e77136e9871fd7aa72c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 15 Aug 2025 12:23:27 +0300 Subject: [PATCH 5/7] mypy fixes --- examples/basic.py | 3 +++ fast_cache_middleware/middleware.py | 12 ++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/basic.py b/examples/basic.py index f7e5af6..b5f39e3 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -122,6 +122,9 @@ async def get_user_in_org(org_id: int, user_id: int) -> UserResponse: Пример более сложного пути с несколькими параметрами. """ user = _USERS_STORAGE.get(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return UserResponse(user_id=user_id, name=user.name, email=user.email) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 0a70262..5e63e9d 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -313,11 +313,6 @@ def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo cache_drop_config.paths if cache_drop_config else None ), ) - logger.debug( - "Extracted cache configuration: %s for paths %s", - cache_configuration.invalidate_paths, - route.name, - ) route_info = RouteInfo( route=route, cache_config=cache_configuration, @@ -360,13 +355,13 @@ def _extract_cache_configs_from_route( def _convert_methods_to_path( self, route_names: dict[str, str], - cache_drop_config: CacheDropConfig, + cache_drop_config: CacheDropConfig | None, ) -> list[re.Pattern] | None: if not cache_drop_config: return None for method in cache_drop_config.methods: - name = getattr(method, "__name__", None) + name = getattr(method, "__name__") route = route_names.get(name) if not route: continue @@ -377,9 +372,6 @@ def _convert_methods_to_path( first_seg = m.group(1) pattern_path = rf"^{re.escape(first_seg)}(?:/|$)" - logger.debug( - "Route '%s' -> first segment pattern '%s'", route, pattern_path - ) cache_drop_config.paths.append(re.compile(pattern_path)) return cache_drop_config.paths From 34a55cbbb60491c34cc03ec52c4d8388aff3a0b3 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Wed, 27 Aug 2025 12:58:06 +0300 Subject: [PATCH 6/7] use starlette methods instead of regex --- fast_cache_middleware/middleware.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 5e63e9d..c5c30a1 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -6,7 +6,7 @@ from fastapi import FastAPI, routing from starlette.requests import Request from starlette.responses import Response -from starlette.routing import Match, Mount +from starlette.routing import Match, Mount, compile_path, get_name from starlette.types import ASGIApp, Receive, Scope, Send from ._helpers import set_cache_age_in_openapi_schema @@ -360,19 +360,22 @@ def _convert_methods_to_path( if not cache_drop_config: return None + seen: set[str] = set() + for method in cache_drop_config.methods: - name = getattr(method, "__name__") + name = get_name(method) route = route_names.get(name) if not route: continue - m = re.match(r"(/[^/]+)", route) - if not m: + regex = compile_path(route)[0] + key = regex.pattern + + if key in seen: continue - first_seg = m.group(1) + seen.add(key) - pattern_path = rf"^{re.escape(first_seg)}(?:/|$)" - cache_drop_config.paths.append(re.compile(pattern_path)) + cache_drop_config.paths.append(regex) return cache_drop_config.paths From ec6d5772a716c73f6d0597d3617159356b96e02f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 5 Sep 2025 11:07:30 +0300 Subject: [PATCH 7/7] clean def --- fast_cache_middleware/middleware.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index c5c30a1..859c09a 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -303,7 +303,10 @@ def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo cache_drop_config, ) = self._extract_cache_configs_from_route(route) - self._convert_methods_to_path(route_names, cache_drop_config) + paths = self._convert_methods_to_path(route_names, cache_drop_config) + + if cache_drop_config and paths is not None: + cache_drop_config.paths.extend(paths) if cache_config or cache_drop_config: cache_configuration = CacheConfiguration( @@ -360,7 +363,7 @@ def _convert_methods_to_path( if not cache_drop_config: return None - seen: set[str] = set() + unique: dict[str, re.Pattern] = {} for method in cache_drop_config.methods: name = get_name(method) @@ -371,13 +374,10 @@ def _convert_methods_to_path( regex = compile_path(route)[0] key = regex.pattern - if key in seen: - continue - seen.add(key) - - cache_drop_config.paths.append(regex) + if key not in unique: + unique[key] = regex - return cache_drop_config.paths + return list(unique.values()) def _find_matching_route( self, request: Request, routes_info: list[RouteInfo]