from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
-from fastapi.utils import get_flat_models_from_routes, get_model_definitions
+from fastapi.utils import (
+ generate_operation_id_for_path,
+ get_flat_models_from_routes,
+ get_model_definitions,
+)
from pydantic.fields import Field
from pydantic.schema import field_schema, get_model_name_map
from pydantic.utils import lenient_issubclass
if route.operation_id:
return route.operation_id
path: str = route.path_format
- operation_id = route.name + path
- operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
- operation_id = operation_id + "_" + method.lower()
- return operation_id
+ return generate_operation_id_for_path(name=route.name, path=path, method=method)
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
)
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
-from fastapi.utils import create_cloned_field
+from fastapi.utils import create_cloned_field, generate_operation_id_for_path
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
self.path = path
self.endpoint = endpoint
self.name = get_name(endpoint) if name is None else name
+ self.path_regex, self.path_format, self.param_convertors = compile_path(path)
+ if methods is None:
+ methods = ["GET"]
+ self.methods = set([method.upper() for method in methods])
+ self.unique_id = generate_operation_id_for_path(
+ name=self.name, path=self.path_format, method=list(methods)[0]
+ )
self.response_model = response_model
if self.response_model:
assert lenient_issubclass(
response_class, JSONResponse
), "To declare a type the response must be a JSON response"
- response_name = "Response_" + self.name
+ response_name = "Response_" + self.unique_id
self.response_field: Optional[Field] = Field(
name=response_name,
type_=self.response_model,
assert lenient_issubclass(
model, BaseModel
), "A response model must be a Pydantic model"
- response_name = f"Response_{additional_status_code}_{self.name}"
+ response_name = f"Response_{additional_status_code}_{self.unique_id}"
response_field = Field(
name=response_name,
type_=model,
else:
self.response_fields = {}
self.deprecated = deprecated
- if methods is None:
- methods = ["GET"]
- self.methods = set([method.upper() for method in methods])
self.operation_id = operation_id
self.response_model_include = response_model_include
self.response_model_exclude = response_model_exclude
self.include_in_schema = include_in_schema
self.response_class = response_class
- self.path_regex, self.path_format, self.param_convertors = compile_path(path)
assert inspect.isfunction(endpoint) or inspect.ismethod(
endpoint
), f"An endpoint must be a function or method"
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
- self.body_field = get_body_field(dependant=self.dependant, name=self.name)
+ self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
self.dependency_overrides_provider = dependency_overrides_provider
self.app = request_response(
get_app(
new_field.shape = field.shape
new_field._populate_validators()
return new_field
+
+
+def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
+ operation_id = name + path
+ operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
+ operation_id = operation_id + "_" + method.lower()
+ return operation_id
--- /dev/null
+from fastapi import APIRouter, Body
+
+router = APIRouter()
+
+
+@router.post("/compute")
+def compute(a: int = Body(...), b: str = Body(...)):
+ return {"a": a, "b": b}
--- /dev/null
+from fastapi import APIRouter, Body
+
+router = APIRouter()
+
+
+@router.post("/compute/")
+def compute(a: int = Body(...), b: str = Body(...)):
+ return {"a": a, "b": b}
--- /dev/null
+from fastapi import FastAPI
+
+from . import a, b
+
+app = FastAPI()
+
+app.include_router(a.router, prefix="/a")
+app.include_router(b.router, prefix="/b")
--- /dev/null
+from starlette.testclient import TestClient
+
+from .app.main import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "Fast API", "version": "0.1.0"},
+ "paths": {
+ "/a/compute": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Compute",
+ "operationId": "compute_a_compute_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_compute_a_compute_post"
+ }
+ }
+ },
+ "required": True,
+ },
+ }
+ },
+ "/b/compute/": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Compute",
+ "operationId": "compute_b_compute__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_compute_b_compute__post"
+ }
+ }
+ },
+ "required": True,
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_compute_b_compute__post": {
+ "title": "Body_compute_b_compute__post",
+ "required": ["a", "b"],
+ "type": "object",
+ "properties": {
+ "a": {"title": "A", "type": "integer"},
+ "b": {"title": "B", "type": "string"},
+ },
+ },
+ "Body_compute_a_compute_post": {
+ "title": "Body_compute_a_compute_post",
+ "required": ["a", "b"],
+ "type": "object",
+ "properties": {
+ "a": {"title": "A", "type": "integer"},
+ "b": {"title": "B", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == openapi_schema
+
+
+def test_post_a():
+ data = {"a": 2, "b": "foo"}
+ response = client.post("/a/compute", json=data)
+ assert response.status_code == 200
+ data = response.json()
+
+
+def test_post_a_invalid():
+ data = {"a": "bar", "b": "foo"}
+ response = client.post("/a/compute", json=data)
+ assert response.status_code == 422
+
+
+def test_post_b():
+ data = {"a": 2, "b": "foo"}
+ response = client.post("/b/compute/", json=data)
+ assert response.status_code == 200
+ data = response.json()
+
+
+def test_post_b_invalid():
+ data = {"a": "bar", "b": "foo"}
+ response = client.post("/b/compute/", json=data)
+ assert response.status_code == 422
"content": {
"application/x-www-form-urlencoded": {
"schema": {
- "$ref": "#/components/schemas/Body_read_current_user"
+ "$ref": "#/components/schemas/Body_read_current_user_login_post"
}
}
},
},
"components": {
"schemas": {
- "Body_read_current_user": {
- "title": "Body_read_current_user",
+ "Body_read_current_user_login_post": {
+ "title": "Body_read_current_user_login_post",
"required": ["grant_type", "username", "password"],
"type": "object",
"properties": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
- "$ref": "#/components/schemas/Body_read_current_user"
+ "$ref": "#/components/schemas/Body_read_current_user_login_post"
}
}
},
},
"components": {
"schemas": {
- "Body_read_current_user": {
- "title": "Body_read_current_user",
+ "Body_read_current_user_login_post": {
+ "title": "Body_read_current_user_login_post",
"required": ["grant_type", "username", "password"],
"type": "object",
"properties": {
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Notes",
+ "title": "Response_Read_Notes_Notes__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Note"},
}
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Body_update_item"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
+ }
}
},
"required": True,
"full_name": {"title": "Full_Name", "type": "string"},
},
},
- "Body_update_item": {
- "title": "Body_update_item",
+ "Body_update_item_items__item_id__put": {
+ "title": "Body_update_item_items__item_id__put",
"required": ["item", "user", "importance"],
"type": "object",
"properties": {
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Body_update_item"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
+ }
}
},
"required": True,
"tax": {"title": "Tax", "type": "number"},
},
},
- "Body_update_item": {
- "title": "Body_update_item",
+ "Body_update_item_items__item_id__put": {
+ "title": "Body_update_item_items__item_id__put",
"required": ["item"],
"type": "object",
"properties": {"item": {"$ref": "#/components/schemas/Item"}},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Body_read_items"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
}
}
},
},
"components": {
"schemas": {
- "Body_read_items": {
- "title": "Body_read_items",
+ "Body_read_items_items__item_id__put": {
+ "title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
"start_datetime": {
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Item",
+ "title": "Response_Read_Item_Items__Item_Id__Get",
"anyOf": [
{"$ref": "#/components/schemas/PlaneItem"},
{"$ref": "#/components/schemas/CarItem"},
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Items",
+ "title": "Response_Read_Items_Items__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Keyword_Weights",
+ "title": "Response_Read_Keyword_Weights_Keyword-Weights__Get",
"type": "object",
"additionalProperties": {"type": "number"},
}
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {"$ref": "#/components/schemas/Body_create_file"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
}
},
"required": True,
"content": {
"multipart/form-data": {
"schema": {
- "$ref": "#/components/schemas/Body_create_upload_file"
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
}
}
},
},
"components": {
"schemas": {
- "Body_create_file": {
- "title": "Body_create_file",
+ "Body_create_upload_file_uploadfile__post": {
+ "title": "Body_create_upload_file_uploadfile__post",
"required": ["file"],
"type": "object",
"properties": {
"file": {"title": "File", "type": "string", "format": "binary"}
},
},
- "Body_create_upload_file": {
- "title": "Body_create_upload_file",
+ "Body_create_file_files__post": {
+ "title": "Body_create_file_files__post",
"required": ["file"],
"type": "object",
"properties": {
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {"$ref": "#/components/schemas/Body_create_files"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_create_files_files__post"
+ }
}
},
"required": True,
"content": {
"multipart/form-data": {
"schema": {
- "$ref": "#/components/schemas/Body_create_upload_files"
+ "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
}
}
},
},
"components": {
"schemas": {
- "Body_create_files": {
- "title": "Body_create_files",
+ "Body_create_upload_files_uploadfiles__post": {
+ "title": "Body_create_upload_files_uploadfiles__post",
"required": ["files"],
"type": "object",
"properties": {
}
},
},
- "Body_create_upload_files": {
- "title": "Body_create_upload_files",
+ "Body_create_files_files__post": {
+ "title": "Body_create_files_files__post",
"required": ["files"],
"type": "object",
"properties": {
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
- "schema": {"$ref": "#/components/schemas/Body_login"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_login_login__post"
+ }
}
},
"required": True,
},
"components": {
"schemas": {
- "Body_login": {
- "title": "Body_login",
+ "Body_login_login__post": {
+ "title": "Body_login_login__post",
"required": ["username", "password"],
"type": "object",
"properties": {
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {"$ref": "#/components/schemas/Body_create_file"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
}
},
"required": True,
},
"components": {
"schemas": {
- "Body_create_file": {
- "title": "Body_create_file",
+ "Body_create_file_files__post": {
+ "title": "Body_create_file_files__post",
"required": ["file", "fileb", "token"],
"type": "object",
"properties": {
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
- "schema": {"$ref": "#/components/schemas/Body_login"}
+ "schema": {
+ "$ref": "#/components/schemas/Body_login_token_post"
+ }
}
},
"required": True,
},
"components": {
"schemas": {
- "Body_login": {
- "title": "Body_login",
+ "Body_login_token_post": {
+ "title": "Body_login_token_post",
"required": ["username", "password"],
"type": "object",
"properties": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
- "$ref": "#/components/schemas/Body_login_for_access_token"
+ "$ref": "#/components/schemas/Body_login_for_access_token_token_post"
}
}
},
"token_type": {"title": "Token_Type", "type": "string"},
},
},
- "Body_login_for_access_token": {
- "title": "Body_login_for_access_token",
+ "Body_login_for_access_token_token_post": {
+ "title": "Body_login_for_access_token_token_post",
"required": ["username", "password"],
"type": "object",
"properties": {
}
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == openapi_schema
+
+
def get_access_token(username="johndoe", password="secret", scope=None):
data = {"username": username, "password": password}
if scope:
return access_token
-def test_openapi_schema():
- response = client.get("/openapi.json")
- assert response.status_code == 200
- assert response.json() == openapi_schema
-
-
def test_login():
response = client.post("/token", data={"username": "johndoe", "password": "secret"})
assert response.status_code == 200
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Users",
+ "title": "Response_Read_Users_Users__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/User"},
}
"content": {
"application/json": {
"schema": {
- "title": "Response_Read_Items",
+ "title": "Response_Read_Items_Items__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}