From 4dd06fb5ceb361163a5fd8417d57be5a5eaf3fcc Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 22 Jun 2025 21:27:13 +0300 Subject: [PATCH 01/16] add methods for set cache_age in openapi schema --- fast_cache_middleware/middleware.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 239192b..6b519b8 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -3,6 +3,7 @@ import typing as tp from fastapi import FastAPI, routing +from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import Response from starlette.routing import Mount @@ -187,6 +188,7 @@ def __init__( async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: app_routes = get_app_routes(scope["app"]) + self.set_cache_age_at_openapi_schema(scope["app"]) self._routes_info = self._extract_routes_info(app_routes) return None @@ -232,6 +234,37 @@ async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | No ) return True + + def set_cache_age_in_openapi_schema(self, app: FastAPI) -> None: + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + for route in app.routes: + if isinstance(route, routing.APIRoute): + path = route.path + methods = route.methods + + for dependency in route.dependant.dependencies: + dep = dependency.call + if isinstance(dep, CacheConfig): + max_age = dep.max_age + + for method in methods: + method = method.lower() + try: + operation = openapi_schema["paths"][path][method] + operation.setdefault("x-cache-age", max_age) + except KeyError: + continue + + app.openapi_schema = openapi_schema + return None + def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo]: """Recursively extracts route information and their dependencies. From 8210ca9e5cca2cf3f5f282575dff4b6dfe8fbade Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 09:58:42 +0300 Subject: [PATCH 02/16] move new methods to helpers module --- fast_cache_middleware/_helpers.py | 35 +++++++++++++++++++++++++++++ fast_cache_middleware/middleware.py | 35 ++--------------------------- 2 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 fast_cache_middleware/_helpers.py diff --git a/fast_cache_middleware/_helpers.py b/fast_cache_middleware/_helpers.py new file mode 100644 index 0000000..a8f260c --- /dev/null +++ b/fast_cache_middleware/_helpers.py @@ -0,0 +1,35 @@ +from fastapi import routing, FastAPI +from fastapi.openapi.utils import get_openapi + +from .depends import CacheConfig + + +def set_cache_age_in_openapi_schema(app: FastAPI) -> None: + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + for route in app.routes: + if isinstance(route, routing.APIRoute): + path = route.path + methods = route.methods + + for dependency in route.dependant.dependencies: + dep = dependency.call + if isinstance(dep, CacheConfig): + max_age = dep.max_age + + for method in methods: + method = method.lower() + try: + operation = openapi_schema["paths"][path][method] + operation.setdefault("x-cache-age", max_age) + except KeyError: + continue + + app.openapi_schema = openapi_schema + return None \ No newline at end of file diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 6b519b8..5baadf3 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -3,7 +3,6 @@ import typing as tp from fastapi import FastAPI, routing -from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import Response from starlette.routing import Mount @@ -13,6 +12,7 @@ from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig from .schemas import RouteInfo from .storages import BaseStorage, InMemoryStorage +from ._helpers import set_cache_age_in_openapi_schema logger = logging.getLogger(__name__) @@ -188,7 +188,7 @@ def __init__( async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: app_routes = get_app_routes(scope["app"]) - self.set_cache_age_at_openapi_schema(scope["app"]) + set_cache_age_in_openapi_schema(scope["app"]) self._routes_info = self._extract_routes_info(app_routes) return None @@ -234,37 +234,6 @@ async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | No ) return True - - def set_cache_age_in_openapi_schema(self, app: FastAPI) -> None: - - openapi_schema = get_openapi( - title=app.title, - version=app.version, - description=app.description, - routes=app.routes, - ) - - for route in app.routes: - if isinstance(route, routing.APIRoute): - path = route.path - methods = route.methods - - for dependency in route.dependant.dependencies: - dep = dependency.call - if isinstance(dep, CacheConfig): - max_age = dep.max_age - - for method in methods: - method = method.lower() - try: - operation = openapi_schema["paths"][path][method] - operation.setdefault("x-cache-age", max_age) - except KeyError: - continue - - app.openapi_schema = openapi_schema - return None - def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo]: """Recursively extracts route information and their dependencies. From 7de47a95e5b4a343528e7aba0be2ce785c6b0496 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:22:54 +0300 Subject: [PATCH 03/16] init method at method --- fast_cache_middleware/middleware.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 5baadf3..1d941bd 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -176,6 +176,7 @@ def __init__( self.storage = storage or InMemoryStorage() self.controller = controller or Controller() + self._openapi_initialized = False self._routes_info: list[RouteInfo] = [] @@ -195,6 +196,12 @@ async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | None: request = Request(scope, receive) + if not self._openapi_initialized: + fastapi_app = scope["app"] + if isinstance(fastapi_app, FastAPI): + set_cache_age_in_openapi_schema(fastapi_app) + self._openapi_initialized = True + # Find matching route route_info = self._find_matching_route(request, self._routes_info) if not route_info: From 4ca24d124e618e6ec7d0e82288f879212fc14bc0 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:24:02 +0300 Subject: [PATCH 04/16] add unit-test for set_cache_age_in_openapi_schema --- tests/test_middleware.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 96e1dbf..281b730 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -42,6 +42,10 @@ async def get_second_user(storage: dict = Depends(get_storage_depends)): return await get_user(2, storage=storage) +async def hidden_route(): + return {"hidden": True} + + @pytest.fixture def app() -> FastAPI: _storage = { @@ -83,6 +87,13 @@ def app() -> FastAPI: dependencies=[CacheDropConfig(paths=["/users/"])], methods={HTTPMethod.DELETE.value}, ) + app.router.add_api_route( + "/no-docs", + hidden_route, + include_in_schema=False, + dependencies=[CacheConfig(max_age=42)], + methods={HTTPMethod.GET.value}, + ) second_app = FastAPI() second_app.add_middleware(FastCacheMiddleware) @@ -182,3 +193,29 @@ def test_middleware_isolated(client: TestClient) -> None: response2 = client.get("/subapp/users/second").json() assert response1["timestamp"] != response2["timestamp"] + + +def test_set_cache_age_to_openapi_schema(app: FastAPI, client: TestClient) -> None: + path = "/users/second" + method = "get" + + client.get(path) + schema = app.openapi() + + assert path in schema["paths"] + assert method in schema["paths"][path] + + operation = schema["paths"][path][method] + + assert "x-cache-age" in operation + assert operation["x-cache-age"] == 5 + + +def test_openapi_patch_keyerror_handled_gracefully( + app: FastAPI, client: TestClient +) -> None: + path = "/no-docs" + + client.get(path) + schema = app.openapi() + assert path not in schema["paths"] From b1ead2bd0adb6adde2e0712cc1c090e5a0bdc5b7 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:39:47 +0300 Subject: [PATCH 05/16] change deprecated datetime.now to actual --- tests/test_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index 85c718c..d3dcd19 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,7 +3,7 @@ import asyncio import time import typing as tp -from datetime import datetime, timedelta +from datetime import datetime, UTC from unittest.mock import AsyncMock, MagicMock import pytest @@ -163,7 +163,7 @@ async def test_get_cached_response_success( """Тестирует успешное получение кешированного ответа.""" cached_response = Response(content="cached", status_code=200) metadata = { - "cached_at": datetime.utcnow().isoformat(), + "cached_at": datetime.now(UTC).isoformat(), "ttl": 300, "etag": "test-etag", } From c73b6dddf70720766d47d985f11c442905bfc3ed Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:40:01 +0300 Subject: [PATCH 06/16] lint fix --- fast_cache_middleware/_helpers.py | 5 ++--- fast_cache_middleware/middleware.py | 2 +- tests/test_controller.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fast_cache_middleware/_helpers.py b/fast_cache_middleware/_helpers.py index a8f260c..90a9d28 100644 --- a/fast_cache_middleware/_helpers.py +++ b/fast_cache_middleware/_helpers.py @@ -1,11 +1,10 @@ -from fastapi import routing, FastAPI +from fastapi import FastAPI, routing from fastapi.openapi.utils import get_openapi from .depends import CacheConfig def set_cache_age_in_openapi_schema(app: FastAPI) -> None: - openapi_schema = get_openapi( title=app.title, version=app.version, @@ -32,4 +31,4 @@ def set_cache_age_in_openapi_schema(app: FastAPI) -> None: continue app.openapi_schema = openapi_schema - return None \ No newline at end of file + return None diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 1d941bd..da08af4 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -8,11 +8,11 @@ from starlette.routing import Mount from starlette.types import ASGIApp, Receive, Scope, Send +from ._helpers import set_cache_age_in_openapi_schema from .controller import Controller from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig from .schemas import RouteInfo from .storages import BaseStorage, InMemoryStorage -from ._helpers import set_cache_age_in_openapi_schema logger = logging.getLogger(__name__) diff --git a/tests/test_controller.py b/tests/test_controller.py index d3dcd19..8162079 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,7 +3,7 @@ import asyncio import time import typing as tp -from datetime import datetime, UTC +from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock import pytest From b19e7f265a24bd255c8ad5997ff18cec672780f3 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 22 Jun 2025 21:27:13 +0300 Subject: [PATCH 07/16] add methods for set cache_age in openapi schema --- fast_cache_middleware/middleware.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 8651143..bb8985b 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -3,6 +3,7 @@ import typing as tp from fastapi import FastAPI, routing +from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import Response from starlette.routing import Mount, Match @@ -229,6 +230,7 @@ def __init__( async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: app_routes = get_app_routes(scope["app"]) + self.set_cache_age_at_openapi_schema(scope["app"]) self._routes_info = self._extract_routes_info(app_routes) return None @@ -276,6 +278,37 @@ async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | No )() return True + + def set_cache_age_in_openapi_schema(self, app: FastAPI) -> None: + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + for route in app.routes: + if isinstance(route, routing.APIRoute): + path = route.path + methods = route.methods + + for dependency in route.dependant.dependencies: + dep = dependency.call + if isinstance(dep, CacheConfig): + max_age = dep.max_age + + for method in methods: + method = method.lower() + try: + operation = openapi_schema["paths"][path][method] + operation.setdefault("x-cache-age", max_age) + except KeyError: + continue + + app.openapi_schema = openapi_schema + return None + def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo]: """Recursively extracts route information and their dependencies. From 4ab16801061f505b9cb14e5216297cc4353405c4 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 09:58:42 +0300 Subject: [PATCH 08/16] move new methods to helpers module --- fast_cache_middleware/_helpers.py | 35 +++++++++++++++++++++++++++++ fast_cache_middleware/middleware.py | 35 ++--------------------------- 2 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 fast_cache_middleware/_helpers.py diff --git a/fast_cache_middleware/_helpers.py b/fast_cache_middleware/_helpers.py new file mode 100644 index 0000000..a8f260c --- /dev/null +++ b/fast_cache_middleware/_helpers.py @@ -0,0 +1,35 @@ +from fastapi import routing, FastAPI +from fastapi.openapi.utils import get_openapi + +from .depends import CacheConfig + + +def set_cache_age_in_openapi_schema(app: FastAPI) -> None: + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + for route in app.routes: + if isinstance(route, routing.APIRoute): + path = route.path + methods = route.methods + + for dependency in route.dependant.dependencies: + dep = dependency.call + if isinstance(dep, CacheConfig): + max_age = dep.max_age + + for method in methods: + method = method.lower() + try: + operation = openapi_schema["paths"][path][method] + operation.setdefault("x-cache-age", max_age) + except KeyError: + continue + + app.openapi_schema = openapi_schema + return None \ No newline at end of file diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index bb8985b..75c84ca 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -3,7 +3,6 @@ import typing as tp from fastapi import FastAPI, routing -from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import Response from starlette.routing import Mount, Match @@ -13,6 +12,7 @@ from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig from .schemas import RouteInfo from .storages import BaseStorage, InMemoryStorage +from ._helpers import set_cache_age_in_openapi_schema logger = logging.getLogger(__name__) @@ -230,7 +230,7 @@ def __init__( async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: app_routes = get_app_routes(scope["app"]) - self.set_cache_age_at_openapi_schema(scope["app"]) + set_cache_age_in_openapi_schema(scope["app"]) self._routes_info = self._extract_routes_info(app_routes) return None @@ -278,37 +278,6 @@ async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | No )() return True - - def set_cache_age_in_openapi_schema(self, app: FastAPI) -> None: - - openapi_schema = get_openapi( - title=app.title, - version=app.version, - description=app.description, - routes=app.routes, - ) - - for route in app.routes: - if isinstance(route, routing.APIRoute): - path = route.path - methods = route.methods - - for dependency in route.dependant.dependencies: - dep = dependency.call - if isinstance(dep, CacheConfig): - max_age = dep.max_age - - for method in methods: - method = method.lower() - try: - operation = openapi_schema["paths"][path][method] - operation.setdefault("x-cache-age", max_age) - except KeyError: - continue - - app.openapi_schema = openapi_schema - return None - def _extract_routes_info(self, routes: list[routing.APIRoute]) -> list[RouteInfo]: """Recursively extracts route information and their dependencies. From fbc77465e979a406ba79993c0c64b105d3fe2a3f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:22:54 +0300 Subject: [PATCH 09/16] init method at method --- fast_cache_middleware/middleware.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 75c84ca..99f56c0 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -218,6 +218,7 @@ def __init__( self.storage = storage or InMemoryStorage() self.controller = controller or Controller() + self._openapi_initialized = False self._routes_info: list[RouteInfo] = [] @@ -237,6 +238,12 @@ async def on_lifespan(self, scope: Scope, _: Receive, __: Send) -> bool | None: async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | None: request = Request(scope, receive) + if not self._openapi_initialized: + fastapi_app = scope["app"] + if isinstance(fastapi_app, FastAPI): + set_cache_age_in_openapi_schema(fastapi_app) + self._openapi_initialized = True + # Find matching route route_info = self._find_matching_route(request, self._routes_info) if not route_info: From 2c5afec23f3229236300a21c13e1cde00895b98f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:24:02 +0300 Subject: [PATCH 10/16] add unit-test for set_cache_age_in_openapi_schema --- tests/test_middleware.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 96e1dbf..281b730 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -42,6 +42,10 @@ async def get_second_user(storage: dict = Depends(get_storage_depends)): return await get_user(2, storage=storage) +async def hidden_route(): + return {"hidden": True} + + @pytest.fixture def app() -> FastAPI: _storage = { @@ -83,6 +87,13 @@ def app() -> FastAPI: dependencies=[CacheDropConfig(paths=["/users/"])], methods={HTTPMethod.DELETE.value}, ) + app.router.add_api_route( + "/no-docs", + hidden_route, + include_in_schema=False, + dependencies=[CacheConfig(max_age=42)], + methods={HTTPMethod.GET.value}, + ) second_app = FastAPI() second_app.add_middleware(FastCacheMiddleware) @@ -182,3 +193,29 @@ def test_middleware_isolated(client: TestClient) -> None: response2 = client.get("/subapp/users/second").json() assert response1["timestamp"] != response2["timestamp"] + + +def test_set_cache_age_to_openapi_schema(app: FastAPI, client: TestClient) -> None: + path = "/users/second" + method = "get" + + client.get(path) + schema = app.openapi() + + assert path in schema["paths"] + assert method in schema["paths"][path] + + operation = schema["paths"][path][method] + + assert "x-cache-age" in operation + assert operation["x-cache-age"] == 5 + + +def test_openapi_patch_keyerror_handled_gracefully( + app: FastAPI, client: TestClient +) -> None: + path = "/no-docs" + + client.get(path) + schema = app.openapi() + assert path not in schema["paths"] From 9290d0adc6f162b48565578f68641190b47c2ae7 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:39:47 +0300 Subject: [PATCH 11/16] change deprecated datetime.now to actual --- tests/test_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index 85c718c..d3dcd19 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,7 +3,7 @@ import asyncio import time import typing as tp -from datetime import datetime, timedelta +from datetime import datetime, UTC from unittest.mock import AsyncMock, MagicMock import pytest @@ -163,7 +163,7 @@ async def test_get_cached_response_success( """Тестирует успешное получение кешированного ответа.""" cached_response = Response(content="cached", status_code=200) metadata = { - "cached_at": datetime.utcnow().isoformat(), + "cached_at": datetime.now(UTC).isoformat(), "ttl": 300, "etag": "test-etag", } From c59e88a008b3701f676bd26016b25e7b8005c31a Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 24 Jun 2025 10:40:01 +0300 Subject: [PATCH 12/16] lint fix --- fast_cache_middleware/_helpers.py | 5 ++--- fast_cache_middleware/middleware.py | 2 +- tests/test_controller.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fast_cache_middleware/_helpers.py b/fast_cache_middleware/_helpers.py index a8f260c..90a9d28 100644 --- a/fast_cache_middleware/_helpers.py +++ b/fast_cache_middleware/_helpers.py @@ -1,11 +1,10 @@ -from fastapi import routing, FastAPI +from fastapi import FastAPI, routing from fastapi.openapi.utils import get_openapi from .depends import CacheConfig def set_cache_age_in_openapi_schema(app: FastAPI) -> None: - openapi_schema = get_openapi( title=app.title, version=app.version, @@ -32,4 +31,4 @@ def set_cache_age_in_openapi_schema(app: FastAPI) -> None: continue app.openapi_schema = openapi_schema - return None \ No newline at end of file + return None diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index 99f56c0..aea8b21 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -8,11 +8,11 @@ from starlette.routing import Mount, Match from starlette.types import ASGIApp, Receive, Scope, Send +from ._helpers import set_cache_age_in_openapi_schema from .controller import Controller from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig from .schemas import RouteInfo from .storages import BaseStorage, InMemoryStorage -from ._helpers import set_cache_age_in_openapi_schema logger = logging.getLogger(__name__) diff --git a/tests/test_controller.py b/tests/test_controller.py index d3dcd19..8162079 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,7 +3,7 @@ import asyncio import time import typing as tp -from datetime import datetime, UTC +from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock import pytest From 68138f02876f9257cf726aa76884449c25dc0bae Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Wed, 25 Jun 2025 18:03:22 +0300 Subject: [PATCH 13/16] skip app check --- fast_cache_middleware/middleware.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fast_cache_middleware/middleware.py b/fast_cache_middleware/middleware.py index b589d61..2df4f3f 100644 --- a/fast_cache_middleware/middleware.py +++ b/fast_cache_middleware/middleware.py @@ -239,10 +239,8 @@ async def on_http(self, scope: Scope, receive: Receive, send: Send) -> bool | No request = Request(scope, receive) if not self._openapi_initialized: - fastapi_app = scope["app"] - if isinstance(fastapi_app, FastAPI): - set_cache_age_in_openapi_schema(fastapi_app) - self._openapi_initialized = True + set_cache_age_in_openapi_schema(scope["app"]) + self._openapi_initialized = True # Find matching route route_info = self._find_matching_route(request, self._routes_info) From 86fcdb0cbac9c90e3c97846053365463003fbcba Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Wed, 25 Jun 2025 20:06:10 +0300 Subject: [PATCH 14/16] move all for tests to conftest --- tests/conftest.py | 141 +++++++++++++++++++++++++++++++++++++ tests/test_middleware.py | 145 --------------------------------------- 2 files changed, 141 insertions(+), 145 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7be5a19 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,141 @@ +import time +import typing as tp +from http import HTTPMethod +from types import MethodType + +import pytest +from fastapi import Depends, FastAPI, HTTPException +from fastapi.openapi.utils import get_openapi +from starlette.requests import Request +from starlette.testclient import TestClient + +from fast_cache_middleware import CacheConfig, CacheDropConfig, FastCacheMiddleware + + +async def get_storage_depends(request: Request) -> tp.Any: + return request.app.state.storage + + +async def create_user(user_id: int, storage: dict = Depends(get_storage_depends)): + user_name = str(time.time) + storage[user_id] = user_name + return {"user_id": user_id, "name": user_name, "timestamp": time.time()} + + +async def get_user(user_id: int, storage: dict = Depends(get_storage_depends)): + try: + user_name = storage[user_id] + except KeyError: + raise HTTPException(status_code=404, detail="User not found") + return {"user_id": user_id, "name": user_name, "timestamp": time.time()} + + +async def delete_user(user_id: int, storage: dict = Depends(get_storage_depends)): + user_name = storage.pop(user_id) + return {"user_id": user_id, "name": user_name, "timestamp": time.time()} + + +async def get_first_user(storage: dict = Depends(get_storage_depends)): + return await get_user(1, storage=storage) + + +async def get_second_user(storage: dict = Depends(get_storage_depends)): + return await get_user(2, storage=storage) + + +async def hidden_route(): + return {"hidden": True} + + +@pytest.fixture +def app() -> FastAPI: + _storage = { + 1: "first", + 2: "second", + } + + app = FastAPI() + app.add_middleware(FastCacheMiddleware) + app.state.storage = _storage + + app.router.add_api_route( + "/users/first", + get_first_user, + dependencies=[CacheDropConfig(["/users/second"])], + methods={HTTPMethod.GET.value}, + ) + app.router.add_api_route( + "/users/second", + get_second_user, + dependencies=[CacheConfig(max_age=5)], + methods={HTTPMethod.GET.value}, + ) + app.router.add_api_route( + "/users/{user_id}", + get_user, + dependencies=[CacheConfig(max_age=10)], + methods={HTTPMethod.GET.value}, + ) + app.router.add_api_route( + "/users/{user_id}", + create_user, + dependencies=[CacheDropConfig(paths=["/users/"])], + methods={HTTPMethod.POST.value}, + ) + app.router.add_api_route( + "/users/{user_id}", + delete_user, + dependencies=[CacheDropConfig(paths=["/users/"])], + methods={HTTPMethod.DELETE.value}, + ) + app.router.add_api_route( + "/no-docs", + hidden_route, + include_in_schema=False, + dependencies=[CacheConfig(max_age=42)], + methods={HTTPMethod.GET.value}, + ) + + def custom_openapi() -> dict[str, tp.Any]: + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi( + title="Custom title", + version="2.5.0", + summary="This is a very custom OpenAPI schema", + description="Here's a longer description of the custom **OpenAPI** schema", + routes=app.routes, + ) + openapi_schema["info"]["x-logo"] = { + "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" + } + app.openapi_schema = openapi_schema + return app.openapi_schema + + app.openapi = custom_openapi # type: ignore + + second_app = FastAPI() + second_app.add_middleware(FastCacheMiddleware) + second_app.state.storage = _storage + + second_app.router.add_api_route( + "/users/first", + get_first_user, + dependencies=[CacheDropConfig(["/subapp/users/second"])], + methods={HTTPMethod.GET.value}, + ) + second_app.router.add_api_route( + "/users/second", + get_second_user, + dependencies=[CacheConfig(max_age=5)], + methods={HTTPMethod.GET.value}, + ) + app.mount("/subapp", app=second_app) + + return app + + +@pytest.fixture +def client(app: FastAPI) -> TestClient: + """Создает тестовый клиент.""" + return TestClient(app) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 281b730..ad1232b 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,126 +1,7 @@ """Тесты для оптимизированного FastCacheMiddleware.""" -import time -import typing as tp -from http import HTTPMethod - -import pytest -from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.testclient import TestClient -from fast_cache_middleware import CacheConfig, CacheDropConfig, FastCacheMiddleware - - -async def get_storage_depends(request: Request) -> tp.Any: - return request.app.state.storage - - -async def create_user(user_id: int, storage: dict = Depends(get_storage_depends)): - user_name = str(time.time) - storage[user_id] = user_name - return {"user_id": user_id, "name": user_name, "timestamp": time.time()} - - -async def get_user(user_id: int, storage: dict = Depends(get_storage_depends)): - try: - user_name = storage[user_id] - except KeyError: - raise HTTPException(status_code=404, detail="User not found") - return {"user_id": user_id, "name": user_name, "timestamp": time.time()} - - -async def delete_user(user_id: int, storage: dict = Depends(get_storage_depends)): - user_name = storage.pop(user_id) - return {"user_id": user_id, "name": user_name, "timestamp": time.time()} - - -async def get_first_user(storage: dict = Depends(get_storage_depends)): - return await get_user(1, storage=storage) - - -async def get_second_user(storage: dict = Depends(get_storage_depends)): - return await get_user(2, storage=storage) - - -async def hidden_route(): - return {"hidden": True} - - -@pytest.fixture -def app() -> FastAPI: - _storage = { - 1: "first", - 2: "second", - } - - app = FastAPI() - app.add_middleware(FastCacheMiddleware) - app.state.storage = _storage - - app.router.add_api_route( - "/users/first", - get_first_user, - dependencies=[CacheDropConfig(["/users/second"])], - methods={HTTPMethod.GET.value}, - ) - app.router.add_api_route( - "/users/second", - get_second_user, - dependencies=[CacheConfig(max_age=5)], - methods={HTTPMethod.GET.value}, - ) - app.router.add_api_route( - "/users/{user_id}", - get_user, - dependencies=[CacheConfig(max_age=10)], - methods={HTTPMethod.GET.value}, - ) - app.router.add_api_route( - "/users/{user_id}", - create_user, - dependencies=[CacheDropConfig(paths=["/users/"])], - methods={HTTPMethod.POST.value}, - ) - app.router.add_api_route( - "/users/{user_id}", - delete_user, - dependencies=[CacheDropConfig(paths=["/users/"])], - methods={HTTPMethod.DELETE.value}, - ) - app.router.add_api_route( - "/no-docs", - hidden_route, - include_in_schema=False, - dependencies=[CacheConfig(max_age=42)], - methods={HTTPMethod.GET.value}, - ) - - second_app = FastAPI() - second_app.add_middleware(FastCacheMiddleware) - second_app.state.storage = _storage - - second_app.router.add_api_route( - "/users/first", - get_first_user, - dependencies=[CacheDropConfig(["/subapp/users/second"])], - methods={HTTPMethod.GET.value}, - ) - second_app.router.add_api_route( - "/users/second", - get_second_user, - dependencies=[CacheConfig(max_age=5)], - methods={HTTPMethod.GET.value}, - ) - app.mount("/subapp", app=second_app) - - return app - - -@pytest.fixture -def client(app: FastAPI) -> TestClient: - """Создает тестовый клиент.""" - return TestClient(app) - def test_caching_works(client: TestClient) -> None: """Тестирует кеширование""" @@ -193,29 +74,3 @@ def test_middleware_isolated(client: TestClient) -> None: response2 = client.get("/subapp/users/second").json() assert response1["timestamp"] != response2["timestamp"] - - -def test_set_cache_age_to_openapi_schema(app: FastAPI, client: TestClient) -> None: - path = "/users/second" - method = "get" - - client.get(path) - schema = app.openapi() - - assert path in schema["paths"] - assert method in schema["paths"][path] - - operation = schema["paths"][path][method] - - assert "x-cache-age" in operation - assert operation["x-cache-age"] == 5 - - -def test_openapi_patch_keyerror_handled_gracefully( - app: FastAPI, client: TestClient -) -> None: - path = "/no-docs" - - client.get(path) - schema = app.openapi() - assert path not in schema["paths"] From 2656e4e21e256e6fbb0341c96a0aba141a6806ef Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Wed, 25 Jun 2025 20:07:07 +0300 Subject: [PATCH 15/16] work with openapi from app, w/o utils methods --- fast_cache_middleware/_helpers.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/fast_cache_middleware/_helpers.py b/fast_cache_middleware/_helpers.py index 90a9d28..e9809c1 100644 --- a/fast_cache_middleware/_helpers.py +++ b/fast_cache_middleware/_helpers.py @@ -1,24 +1,18 @@ from fastapi import FastAPI, routing -from fastapi.openapi.utils import get_openapi from .depends import CacheConfig def set_cache_age_in_openapi_schema(app: FastAPI) -> None: - openapi_schema = get_openapi( - title=app.title, - version=app.version, - description=app.description, - routes=app.routes, - ) + openapi_schema = app.openapi() for route in app.routes: if isinstance(route, routing.APIRoute): path = route.path methods = route.methods - for dependency in route.dependant.dependencies: - dep = dependency.call + for dependency in route.dependencies: + dep = dependency.dependency if isinstance(dep, CacheConfig): max_age = dep.max_age From b7c10aa5149adc3127699179b5099846c22f4fa5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Wed, 25 Jun 2025 20:07:40 +0300 Subject: [PATCH 16/16] move tests to own module, add new test --- tests/test_openapi_schema.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_openapi_schema.py diff --git a/tests/test_openapi_schema.py b/tests/test_openapi_schema.py new file mode 100644 index 0000000..4776c7a --- /dev/null +++ b/tests/test_openapi_schema.py @@ -0,0 +1,42 @@ +from fastapi import FastAPI +from starlette.testclient import TestClient + + +def test_set_cache_age_to_openapi_schema(app: FastAPI, client: TestClient) -> None: + path = "/users/second" + method = "get" + + client.get(path) + schema = app.openapi() + + assert path in schema["paths"] + assert method in schema["paths"][path] + + operation = schema["paths"][path][method] + + assert "x-cache-age" in operation + assert operation["x-cache-age"] == 5 + + +def test_x_logo_field_exists_after_set_cache_age( + app: FastAPI, client: TestClient +) -> None: + path = "/users/second" + method = "get" + + client.get(path) + schema = app.openapi() + operation = schema["paths"][path][method] + + assert "x-logo" in schema["info"] + assert "x-cache-age" in operation + + +def test_openapi_patch_keyerror_handled_gracefully( + app: FastAPI, client: TestClient +) -> None: + path = "/no-docs" + + client.get(path) + schema = app.openapi() + assert path not in schema["paths"]