FAPI001 endpoint-metadata¶
Co robi¶
Zgłasza operację ścieżki FastAPI (@router.get(...), @app.post(...), @router.api_route(...)), której wpis OpenAPI nie ma metadanych wymaganych przez projekt. Jedna diagnostyka na endpoint, na jego dekoratorze, wymienia wszystko, czego brakuje. Domyślnie reguła wymaga:
- podsumowania:
summary=albo opisu, czylidescription=lub docstringu (FastAPI czyta docstring, gdy nie madescription=, a podsumowanie wraca wtedy do nazwy funkcji); - modelu odpowiedzi:
response_model=albo adnotacji typu zwracanego, z której FastAPI zbuduje schemat, czym nie sąResponsei jego podklasy. Trasa zestatus_code=204jest zwolniona; - jawnego
status_code=dlaPOSTiDELETE, bo 200 rzadko pasuje do tworzenia albo usuwania; - pola
descriptionw każdym wpisieresponses=.
Można też wymagać tagów i operation_id (zobacz Konfigurację). Trasy z include_in_schema=False, w dekoratorze albo w ich APIRouter(...), nie mają wpisu OpenAPI i są pomijane.
Dlaczego to źle¶
Schemat OpenAPI to kontrakt, z którego powstają frontend i każdy wygenerowany klient, a FastAPI wypełnia go tylko tym, co kod deklaruje. Endpoint bez podsumowania pokazuje nazwę funkcji, bez modelu odpowiedzi pusty schemat, a tworzenie zasobu, które odpowiada 200, nic klientowi nie mówi. Agenci dodają endpointy szybko i rzadko wracają po metadane.
Przykład¶
Z trzema warstwami z INW001 włącz FAPI001:
[tool.inwards]
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
[tool.inwards.rules]
extend-select = ["FAPI001"]
Agent dodaje endpoint, który tworzy zamówienie:
from fastapi import APIRouter
router = APIRouter(prefix="/orders", tags=["orders"])
@router.post("/")
async def create_order(body: OrderIn):
...
shop/infrastructure/orders.py:6:1: FAPI001 `create_order` (POST /) has no summary, explicit status code or response model in its OpenAPI metadata.
fix: Declare the missing OpenAPI metadata on `@router.post("/")`.
1. Add `summary="..."` (or a docstring) and `status_code=201` to `@router.post("/")`, and a return annotation or `response_model=...`.
2. Write a summary that says what the endpoint does, not the function name; declare the model the endpoint really returns.
3. If the project doesn't want this metadata, ask the user to change [tool.inwards.rules.endpoint-metadata]; don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/FAPI001/
Poprawiony: endpoint mówi, co robi, co zwraca i jakim statusem odpowiada.
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/orders", tags=["orders"])
class OrderIn(BaseModel):
product_id: int
class OrderOut(BaseModel):
id: int
@router.post("/", status_code=201, summary="Place an order")
async def create_order(body: OrderIn) -> OrderOut:
return OrderOut(id=1)
Jak naprawić¶
- Dodaj do dekoratora to, co wymienia diagnostyka:
summary="..."albo docstring,status_code=,"description"w każdym wpisieresponses=. - Zadeklaruj model, który endpoint zwraca, jako adnotację typu zwracanego albo
response_model=. Bezpośrednio zwracanyResponsenie daje FastAPI niczego do udokumentowania. - Jeśli projekt nie chce któregoś sprawdzenia, wyłącz je w
[tool.inwards.rules.endpoint-metadata]zamiast dopisywać zastępcze metadane.
Bezpieczeństwo poprawki¶
Bez automatycznej poprawki (autofix: false). Podsumowanie i kod statusu to decyzje o API, a nie tekst do wygenerowania.
Konfiguracja¶
Opt-in: reguła zgłasza tylko wtedy, gdy extend-select albo select wymienia FAPI001. Jej opcje z wartościami domyślnymi:
[tool.inwards.rules]
extend-select = ["FAPI001"]
[tool.inwards.rules.endpoint-metadata]
require-summary = "summary-or-docstring" # or "summary" (summary= itself), or false
require-response-model = true # response_model= or a usable return annotation
require-status-code = ["post", "delete"] # methods that need an explicit status_code=
require-response-fields = ["description"] # keys every responses= entry needs: description, model, content
require-tags = false # tags= on the route, its router, or an include_router above it
require-operation-id = false # an explicit operation_id=, for generated clients
false, a dla listy [], wyłącza sprawdzenie. Nieznany klucz albo zła wartość to błąd konfiguracji, który podaje klucz. Jak każda reguła, przyjmuje też modules, a jak każda reguła treści także check-tests, które sprawia, że zgłasza też w modułach testowych (tabele opcji). Diagnostykę można wyciszyć w linii dekoratora przez # inwards: ignore[FAPI001] reason="...".
Znane ograniczenia¶
- Dekorator z
**kwargsjest pomijany: metadane mogą być w środku. require-tagsliczy tagi podane winclude_router(...)w innym pliku; dołączenie, którego Inwards nie umie rozwiązać (na przykład router w pętli), ucisza sprawdzenie.- Reguła sprawdza, czy podsumowanie istnieje, a nie czy jest dobre.
include_in_schema=Falsenainclude_router(...)gdzie indziej jest brane pod uwagę tylko przez dołączenia, które Inwards umie rozwiązać.- Trasy dodawane w czasie działania (
add_api_route, pętle po konfiguracji) nie są widoczne. - Aplikacja, która nie serwuje schematu (
FastAPI(openapi_url=None)), jest pomijana razem ze wszystkimi swoimi trasami, jak przyinclude_in_schema=False. Tak samo router, który każde dołączenie śledzone przez graf umieszcza pod taką aplikacją, nawet gdy innego dołączenia w projekcie nie da się prześledzić: back office Polara.openapi_url=Nonewyłącza tylko serwowanie schematu przez FastAPI:app.openapi()ifastapi.openapi.utils.get_openapinadal budują go ze wszystkich tras zinclude_in_schemaustawionym na prawdę (applications.py,setup()iopenapi()). Dlatego aplikacja, która dodaje własną trasę o ścieżce kończącej się naopenapi.json(app.add_api_route(f"/{version}/openapi.json", ...),@app.get("/openapi.json")), liczy się jako serwująca schemat, tak jak API Polara (#391). Taką trasę można dodać na parametrze funkcji, do której trafia aplikacja. - Które routery się liczą (#358). Trasa liczy się na nazwie powiązanej z
FastAPI(...)alboAPIRouter(...)z FastAPI, a gdy plik wywołuje na tej nazwie dekorator trasy,include_routeralbo inną metodę routera, także z własną klasą albo fabryką routera, którą Inwards rozwiązuje przez importy projektu: podklasą klasy FastAPI;NAZWA = fabryka(...)na poziomie modułu, gdy własna fabryka klas mówi albo pokazuje, że zwraca klasę routera (-> type[APIRouter]albo podklasa, którą definiuje i zwraca, jakget_api_router_classw Polarze); dowolnymAPIRouter = <wywołanie>alboFastAPI = <wywołanie>na poziomie modułu; własną funkcją, która zwraca router, jeden krok w głąb; oraz nazwą zwracaną przez funkcję z adnotacją-> APIRouter(fabrykimake_*_router()w fastapi-clean-example). Samo wywołanie czegoś, co nazywa sięAPIRouter, się nie liczy: projekty OpenStack tak nazywają swoje routery WSGI. Argumenty fabryki nie są słowami kluczowymi routera, więc prefiks, tagi iresponses=routera z fabryki są nieznane, a jego trasy liczą się jako widoczne w schemacie. Plik, który nie zawiera żadnego słowa z filtra tekstowego (fastapi,APIRouter,include_router...), nie jest czytany. - Klasy tras (#392). FastAPI buduje każdą trasę klasą
route_classjej routera i buduje ją ponownie, z połączonymi tagami, przy każdyminclude_router(routing.py,add_api_routeiinclude_router). Klasa tras pochodzi zroute_class=albo z własnej klasy routera, która ją ustawia (kwargs["route_class"] = route_classw fabryce klas, jakget_api_router_classPolara). Gdy jeden__init__wśród jej własnych klas ustawiaself.include_in_schemana podstawie tagów, Inwards wykonuje ten kod dla każdej trasy: łańcuchif/elif/elsepo<tag> in self.tags(albo lokalnej nazwie z nim związanej) iself.include_in_schema, przypisującyTruealboFalse. Tagi to napisy albo elementy własnych enumów przechowujące napis (APITag.privatePolara,StrEnum), z dekoratora, routera i każdegoinclude_routernad nim. Trasa jest pomijana, gdy klasa ją wyklucza, a także gdy odpowiedź jest nieznana: wartość inna niż literał (settings.is_development()Polara dla tras prywatnych), warunek albo instrukcja, której Inwards nie umie odczytać, tag, którego nie umie odczytać, albo klasa tras, która zapisujeself.tags. Klasa tras, która nie dotyka schematu, niczego nie zmienia. - Klasy tras. Własna klasa
route_class=, która zmienia schemat, nie jest czytana: klasa Polara trzyma trasy z tagiemAPITag.privatepoza schematem produkcyjnym, a FAPI001 nadal je zgłasza (#392). - Zmierzona na korpusie (#182, rozdział 6): 22 zgłoszenia przed regułą o
openapi_url, 21 po niej, wszystkie obejrzane: 19 trafnych (POSTalboDELETEbez jawnego kodu statusu, trasa bez podsumowania) i 2 na małych aplikacjach, które budują testy Polara; tych nie zgłasza, odkąd moduły testowe są pomijane (#362). Od #358 reguła widzi też 526 operacji ścieżki Polara poza testami i 15 z fastapi-clean-example. Wszystkie 11 zgłoszeń w fastapi-clean-example dotyczyło tras zdescription=getdoc(...), które teraz liczy się jak docstring, więc nie zostało żadne. 131 zgłoszeń w Polarze, wszystkie sklasyfikowane: 96 na trasach z tagiemAPITag.private(#392) i 35 na trasach publicznych, każde przeczytane: 25 trafnych (POSTalboDELETEbez jawnego kodu statusu, wpisresponses=bez opisu, trasa bez podsumowania) i 10 fałszywych alarmów (4 eksporty CSV i 6 endpointów OAuth2, których treść odpowiedzi definiują RFC). Odkąd #391 śledziadd_versioned_routers(app, api_router)Polara, jego trasy API leżą podFastAPI(openapi_url=None)zcreate_app()i bez opisanej wyżej trasyopenapi.jsonwszystkie byłyby pomijane (0 zgłoszeń); z nią zgłaszanych jest tych samych 131. Odkąd #392 czyta klasę tras Polara, znika 101 zgłoszeń na trasach, które ta klasa wyklucza ze schematu produkcyjnego (prywatnych albo prywatnych w dekoratorze pod publicznym routerem): zostaje 30 zgłoszeń, 20 trafnych i tych samych 10 fałszywych (4 eksporty CSV, 6 endpointów OAuth2), 67 % wobec 19 % wcześniej.
Źródła¶
- ADR-037: Framework rule families, opt-in, with their own prefix
- FastAPI: Path Operation Configuration, Response Model
FAST001Ruffa zgłasza zbędnyresponse_model; FAPI001 zgłasza brakujący.