-
Notifications
You must be signed in to change notification settings - Fork 0
Add methods for set cache_age in openapi schema #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4dd06fb
8210ca9
7de47a9
4ca24d1
b1ead2b
c73b6dd
b19e7f2
4ab1680
fbc7746
2c5afec
9290d0a
c59e88a
1264770
5ebd29f
68138f0
86fcdb0
2656e4e
b7c10aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| from fastapi import FastAPI, routing | ||
|
|
||
| from .depends import CacheConfig | ||
|
|
||
|
|
||
| def set_cache_age_in_openapi_schema(app: FastAPI) -> None: | ||
| openapi_schema = app.openapi() | ||
|
|
||
| for route in app.routes: | ||
| if isinstance(route, routing.APIRoute): | ||
| path = route.path | ||
| methods = route.methods | ||
|
|
||
| for dependency in route.dependencies: | ||
| dep = dependency.dependency | ||
| 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 | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а как она обычно подставляется? мы не ломаем какие то едж кейсы про генерацию схемы?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Скорее всего ещё надо в начале прокинуть проверку схему. Доке ФастАПИ есть пример кастомной схемы - https://fastapi.tiangolo.com/how-to/extending-openapi/#normal-fastapi Там прям так и идёт: Единственное я ничего не возвращаю.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а в
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. бездушная машина тоже говорит
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Там логика такая: мы запрашиваем уже готовую схему от app, её редактируем, добавляем новые поля в ручки и передаём обновлённую схему. В прошлом варианте мы получали эту схему и целиком перезаписывали её, тем самым все пользовательские схемы перетирали. |
||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import time | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Создал conftest, надеюсь не против. А то фикстуры разрастаются, становится тяжко. И свои тесты по схеме запихнул в отдельный модуль. |
||
| 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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Мне никак не победить mypy с его ошибкой Mypy: Cannot assign to a method [method-assign]. |
||
|
|
||
| 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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Добавил тест, который проверяет схему на "добавленные пользователем поля", работаю с функцией из utils, всё перезаписывалось. Переделал что б мы работали чисто со схемой приложения и всё нормально, перезаписей нету.