Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c89dc736bd | ||
|
|
19769d0483 | ||
|
|
586d17bfb5 | ||
|
|
786b5858e5 | ||
|
|
8a0d4c79c1 |
267
docs/en/docs/advanced/generate-clients.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Generate Clients
|
||||
|
||||
As **FastAPI** is based on the OpenAPI specification, you get automatic compatibility with many tools, including the automatic API docs (provided by Swagger UI).
|
||||
|
||||
One particular advantage that is not necessarily obvious is that you can **generate clients** (sometimes called <abbr title="Software Development Kits">**SDKs**</abbr> ) for your API, for many different **programming languages**.
|
||||
|
||||
## OpenAPI Client Generators
|
||||
|
||||
There are many tools to generate clients from **OpenAPI**.
|
||||
|
||||
A common tool is <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>.
|
||||
|
||||
If you are building a **frontend**, a very interesting alternative is <a href="https://github.com/ferdikoomen/openapi-typescript-codegen" class="external-link" target="_blank">openapi-typescript-codegen</a>.
|
||||
|
||||
## Generate a TypeScript Frontend Client
|
||||
|
||||
Let's start with a simple FastAPI application:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="9-11 14-15 18 19 23"
|
||||
{!> ../../../docs_src/generate_clients/tutorial001.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="7-9 12-13 16-17 21"
|
||||
{!> ../../../docs_src/generate_clients/tutorial001_py39.py!}
|
||||
```
|
||||
|
||||
Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`.
|
||||
|
||||
### API Docs
|
||||
|
||||
If you go to the API docs, you will see that it has the **schemas** for the data to be sent in requests and received in responses:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image01.png">
|
||||
|
||||
You can see those schemas because they were declared with the models in the app.
|
||||
|
||||
That information is available in the app's **OpenAPI schema**, and then shown in the API docs (by Swagger UI).
|
||||
|
||||
And that same information from the models that is included in OpenAPI is what can be used to **generate the client code**.
|
||||
|
||||
### Generate a TypeScript Client
|
||||
|
||||
Now that we have the app with the models, we can generate the client code for the frontend.
|
||||
|
||||
#### Install `openapi-typescript-codegen`
|
||||
|
||||
You can install `openapi-typescript-codegen` in your frontend code with:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm install openapi-typescript-codegen --save-dev
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
#### Generate Client Code
|
||||
|
||||
To generate the client code you can use the command line application `openapi` that would now be installed.
|
||||
|
||||
Because it is installed in the local project, you probably wouldn't be able to call that command directly, but you would put it on your `package.json` file.
|
||||
|
||||
It could look like this:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "^0.20.1",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After having that NPM `generate-client` script there, you can run it with:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm run generate-client
|
||||
|
||||
frontend-app@1.0.0 generate-client /home/user/code/frontend-app
|
||||
> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That command will generate code in `./src/client` and will use `axios` (the frontend HTTP library) internally.
|
||||
|
||||
### Try Out the Client Code
|
||||
|
||||
Now you can import and use the client code, it could look like this, notice that you get autocompletion for the methods:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image02.png">
|
||||
|
||||
You will also get autocompletion for the payload to send:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image03.png">
|
||||
|
||||
!!! tip
|
||||
Notice the autocompletion for `name` and `price`, that was defined in the FastAPI application, in the `Item` model.
|
||||
|
||||
You will have inline errors for the data that you send:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image04.png">
|
||||
|
||||
The response object will also have autocompletion:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image05.png">
|
||||
|
||||
## FastAPI App with Tags
|
||||
|
||||
In many cases your FastAPI app will be bigger, and you will probably use tags to separate different groups of *path operations*.
|
||||
|
||||
For example, you could have a section for **items** and another section for **users**, and they could be separated by tags:
|
||||
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="23 28 36"
|
||||
{!> ../../../docs_src/generate_clients/tutorial002.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="21 26 34"
|
||||
{!> ../../../docs_src/generate_clients/tutorial002_py39.py!}
|
||||
```
|
||||
|
||||
### Generate a TypeScript Client with Tags
|
||||
|
||||
If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags.
|
||||
|
||||
This way you will be able to have things ordered and grouped correctly for the client code:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image06.png">
|
||||
|
||||
In this case you have:
|
||||
|
||||
* `ItemsService`
|
||||
* `UsersService`
|
||||
|
||||
### Client Method Names
|
||||
|
||||
Right now the generated method names like `createItemItemsPost` don't look very clean:
|
||||
|
||||
```TypeScript
|
||||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
|
||||
```
|
||||
|
||||
...that's because the client generator uses the OpenAPI internal **operation ID** for each *path operation*.
|
||||
|
||||
OpenAPI requires that each operation ID is unique across all the *path operations*, so FastAPI uses the **function name**, the **path**, and the **HTTP method/operation** to generate that operation ID, because that way it can make sure that the operation IDs are unique.
|
||||
|
||||
But I'll show you how to improve that next. 🤓
|
||||
|
||||
## Custom Operation IDs and Better Method Names
|
||||
|
||||
You can **modify** the way these operation IDs are **generated** to make them simpler and have **simpler method names** in the clients.
|
||||
|
||||
In this case you will have to ensure that each operation ID is **unique** in some other way.
|
||||
|
||||
For example, you could make sure that each *path operation* has a tag, and then generate the operation ID based on the **tag** and the *path operation* **name** (the function name).
|
||||
|
||||
### Custom Generate Unique ID Function
|
||||
|
||||
FastAPI uses a **unique ID** for each *path operation*, it is used for the **operation ID** and also for the names of any needed custom models, for requests or responses.
|
||||
|
||||
You can customize that function. It takes an `APIRoute` and outputs a string.
|
||||
|
||||
For example, here it is using the first tag (you will probably have only one tag) and the *path operation* name (the function name).
|
||||
|
||||
You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="8-9 12"
|
||||
{!> ../../../docs_src/generate_clients/tutorial003.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="6-7 10"
|
||||
{!> ../../../docs_src/generate_clients/tutorial003_py39.py!}
|
||||
```
|
||||
|
||||
### Generate a TypeScript Client with Custom Operation IDs
|
||||
|
||||
Now if you generate the client again, you will see that it has the improved method names:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image07.png">
|
||||
|
||||
As you see, the method names now have the tag and then the function name, now they don't include information from the URL path and the HTTP operation.
|
||||
|
||||
### Preprocess the OpenAPI Specification for the Client Generator
|
||||
|
||||
The generated code still has some **duplicated information**.
|
||||
|
||||
We already know that this method is related to the **items** because that word is in the `ItemsService` (taken from the tag), but we still have the tag name prefixed in the method name too. 😕
|
||||
|
||||
We will probably still want to keep it for OpenAPI in general, as that will ensure that the operation IDs are **unique**.
|
||||
|
||||
But for the generated client we could **modify** the OpenAPI operation IDs right before generating the clients, just to make those method names nicer and **cleaner**.
|
||||
|
||||
We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this:
|
||||
|
||||
```Python
|
||||
{!../../../docs_src/generate_clients/tutorial004.py!}
|
||||
```
|
||||
|
||||
With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names.
|
||||
|
||||
### Generate a TypeScript Client with the Preprocessed OpenAPI
|
||||
|
||||
Now as the end result is in a file `openapi.json`, you would modify the `package.json` to use that local file, for example:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi --input ./openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "^0.20.1",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After generating the new client, you would now have **clean method names**, with all the **autocompletion**, **inline errors**, etc:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image08.png">
|
||||
|
||||
## Benefits
|
||||
|
||||
When using the automatically generated clients you would **autocompletion** for:
|
||||
|
||||
* Methods.
|
||||
* Request payloads in the body, query parameters, etc.
|
||||
* Response payloads.
|
||||
|
||||
You would also have **inline errors** for everything.
|
||||
|
||||
And whenever you update the backend code, and **regenerate** the frontend, it would have any new *path operations* available as methods, the old ones removed, and any other change would be reflected on the generated code. 🤓
|
||||
|
||||
This also means that if something changed it will be **reflected** on the client code automatically. And if you **build** the client it will error out if you have any **mismatch** in the data used.
|
||||
|
||||
So, you would **detect many errors** very early in the development cycle instead of having to wait for the errors to show up to your final users in production and then trying to debug where the problem is. ✨
|
||||
BIN
docs/en/docs/img/tutorial/generate-clients/image01.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image02.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image03.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image04.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image05.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image06.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image07.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/en/docs/img/tutorial/generate-clients/image08.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -3,6 +3,12 @@
|
||||
## Latest Changes
|
||||
|
||||
|
||||
## 0.75.0
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add support for custom `generate_unique_id_function` and docs for generating clients. New docs: [Advanced - Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/). PR [#4650](https://github.com/tiangolo/fastapi/pull/4650) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.74.1
|
||||
|
||||
### Features
|
||||
|
||||
@@ -140,6 +140,7 @@ nav:
|
||||
- advanced/extending-openapi.md
|
||||
- advanced/openapi-callbacks.md
|
||||
- advanced/wsgi.md
|
||||
- advanced/generate-clients.md
|
||||
- async.md
|
||||
- Deployment:
|
||||
- deployment/index.md
|
||||
|
||||
28
docs_src/generate_clients/tutorial001.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage)
|
||||
async def create_item(item: Item):
|
||||
return {"message": "item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
26
docs_src/generate_clients/tutorial001_py39.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage)
|
||||
async def create_item(item: Item):
|
||||
return {"message": "item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
38
docs_src/generate_clients/tutorial002.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
36
docs_src/generate_clients/tutorial002_py39.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
44
docs_src/generate_clients/tutorial003.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute):
|
||||
return f"{route.tags[0]}-{route.name}"
|
||||
|
||||
|
||||
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
42
docs_src/generate_clients/tutorial003_py39.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute):
|
||||
return f"{route.tags[0]}-{route.name}"
|
||||
|
||||
|
||||
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
class ResponseMessage(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
|
||||
async def create_item(item: Item):
|
||||
return {"message": "Item received"}
|
||||
|
||||
|
||||
@app.get("/items/", response_model=list[Item], tags=["items"])
|
||||
async def get_items():
|
||||
return [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||
|
||||
|
||||
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
|
||||
async def create_user(user: User):
|
||||
return {"message": "User received"}
|
||||
15
docs_src/generate_clients/tutorial004.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
file_path = Path("./openapi.json")
|
||||
openapi_content = json.loads(file_path.read_text())
|
||||
|
||||
for path_data in openapi_content["paths"].values():
|
||||
for operation in path_data.values():
|
||||
tag = operation["tags"][0]
|
||||
operation_id = operation["operationId"]
|
||||
to_remove = f"{tag}-"
|
||||
new_operation_id = operation_id[len(to_remove) :]
|
||||
operation["operationId"] = new_operation_id
|
||||
|
||||
file_path.write_text(json.dumps(openapi_content))
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.74.1"
|
||||
__version__ = "0.75.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from fastapi.openapi.docs import (
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.utils import generate_unique_id
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
@@ -68,10 +69,44 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
**extra: Any,
|
||||
) -> None:
|
||||
self._debug: bool = debug
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.version = version
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license_info = license_info
|
||||
self.openapi_url = openapi_url
|
||||
self.openapi_tags = openapi_tags
|
||||
self.root_path_in_servers = root_path_in_servers
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.swagger_ui_parameters = swagger_ui_parameters
|
||||
self.servers = servers or []
|
||||
self.extra = extra
|
||||
self.openapi_version = "3.0.2"
|
||||
self.openapi_schema: Optional[Dict[str, Any]] = None
|
||||
if self.openapi_url:
|
||||
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
|
||||
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
|
||||
# TODO: remove when discarding the openapi_prefix parameter
|
||||
if openapi_prefix:
|
||||
logger.warning(
|
||||
'"openapi_prefix" has been deprecated in favor of "root_path", which '
|
||||
"follows more closely the ASGI standard, is simpler, and more "
|
||||
"automatic. Check the docs at "
|
||||
"https://fastapi.tiangolo.com/advanced/sub-applications/"
|
||||
)
|
||||
self.root_path = root_path or openapi_prefix
|
||||
self.state: State = State()
|
||||
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes=routes,
|
||||
dependency_overrides_provider=self,
|
||||
@@ -83,6 +118,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
responses=responses,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
self.exception_handlers: Dict[
|
||||
Union[int, Type[Exception]],
|
||||
@@ -99,40 +135,6 @@ class FastAPI(Starlette):
|
||||
[] if middleware is None else list(middleware)
|
||||
)
|
||||
self.middleware_stack: ASGIApp = self.build_middleware_stack()
|
||||
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.version = version
|
||||
self.terms_of_service = terms_of_service
|
||||
self.contact = contact
|
||||
self.license_info = license_info
|
||||
self.servers = servers or []
|
||||
self.openapi_url = openapi_url
|
||||
self.openapi_tags = openapi_tags
|
||||
# TODO: remove when discarding the openapi_prefix parameter
|
||||
if openapi_prefix:
|
||||
logger.warning(
|
||||
'"openapi_prefix" has been deprecated in favor of "root_path", which '
|
||||
"follows more closely the ASGI standard, is simpler, and more "
|
||||
"automatic. Check the docs at "
|
||||
"https://fastapi.tiangolo.com/advanced/sub-applications/"
|
||||
)
|
||||
self.root_path = root_path or openapi_prefix
|
||||
self.root_path_in_servers = root_path_in_servers
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.swagger_ui_parameters = swagger_ui_parameters
|
||||
self.extra = extra
|
||||
self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
|
||||
|
||||
self.openapi_version = "3.0.2"
|
||||
|
||||
if self.openapi_url:
|
||||
assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
|
||||
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
|
||||
self.openapi_schema: Optional[Dict[str, Any]] = None
|
||||
self.setup()
|
||||
|
||||
def build_middleware_stack(self) -> ASGIApp:
|
||||
@@ -286,6 +288,9 @@ class FastAPI(Starlette):
|
||||
),
|
||||
name: Optional[str] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
path,
|
||||
@@ -311,6 +316,7 @@ class FastAPI(Starlette):
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def api_route(
|
||||
@@ -338,6 +344,9 @@ class FastAPI(Starlette):
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
def decorator(func: DecoratedCallable) -> DecoratedCallable:
|
||||
self.router.add_api_route(
|
||||
@@ -364,6 +373,7 @@ class FastAPI(Starlette):
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
return func
|
||||
|
||||
@@ -395,6 +405,9 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
@@ -406,6 +419,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
default_response_class=default_response_class,
|
||||
callbacks=callbacks,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -433,6 +447,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.get(
|
||||
path,
|
||||
@@ -457,6 +474,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -484,6 +502,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.put(
|
||||
path,
|
||||
@@ -508,6 +529,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -535,6 +557,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.post(
|
||||
path,
|
||||
@@ -559,6 +584,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -586,6 +612,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.delete(
|
||||
path,
|
||||
@@ -610,6 +639,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -637,6 +667,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.options(
|
||||
path,
|
||||
@@ -661,6 +694,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -688,6 +722,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.head(
|
||||
path,
|
||||
@@ -712,6 +749,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -739,6 +777,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.patch(
|
||||
path,
|
||||
@@ -763,6 +804,7 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -790,6 +832,9 @@ class FastAPI(Starlette):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.router.trace(
|
||||
path,
|
||||
@@ -814,4 +859,5 @@ class FastAPI(Starlette):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import http.client
|
||||
import inspect
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
@@ -140,7 +141,15 @@ def get_openapi_operation_request_body(
|
||||
return request_body_oai
|
||||
|
||||
|
||||
def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
|
||||
def generate_operation_id(
|
||||
*, route: routing.APIRoute, method: str
|
||||
) -> str: # pragma: nocover
|
||||
warnings.warn(
|
||||
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
||||
"it is not used internally, and will be removed soon",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if route.operation_id:
|
||||
return route.operation_id
|
||||
path: str = route.path_format
|
||||
@@ -154,7 +163,7 @@ def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
|
||||
|
||||
def get_openapi_operation_metadata(
|
||||
*, route: routing.APIRoute, method: str
|
||||
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
|
||||
) -> Dict[str, Any]:
|
||||
operation: Dict[str, Any] = {}
|
||||
if route.tags:
|
||||
@@ -162,14 +171,25 @@ def get_openapi_operation_metadata(
|
||||
operation["summary"] = generate_operation_summary(route=route, method=method)
|
||||
if route.description:
|
||||
operation["description"] = route.description
|
||||
operation["operationId"] = generate_operation_id(route=route, method=method)
|
||||
operation_id = route.operation_id or route.unique_id
|
||||
if operation_id in operation_ids:
|
||||
message = (
|
||||
f"Duplicate Operation ID {operation_id} for function "
|
||||
+ f"{route.endpoint.__name__}"
|
||||
)
|
||||
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
||||
if file_name:
|
||||
message += f" at {file_name}"
|
||||
warnings.warn(message)
|
||||
operation_ids.add(operation_id)
|
||||
operation["operationId"] = operation_id
|
||||
if route.deprecated:
|
||||
operation["deprecated"] = route.deprecated
|
||||
return operation
|
||||
|
||||
|
||||
def get_openapi_path(
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str]
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
@@ -183,7 +203,9 @@ def get_openapi_path(
|
||||
route_response_media_type: Optional[str] = current_response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
operation = get_openapi_operation_metadata(
|
||||
route=route, method=method, operation_ids=operation_ids
|
||||
)
|
||||
parameters: List[Dict[str, Any]] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
@@ -217,7 +239,9 @@ def get_openapi_path(
|
||||
cb_security_schemes,
|
||||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback, model_name_map=model_name_map
|
||||
route=callback,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
@@ -384,6 +408,7 @@ def get_openapi(
|
||||
output["servers"] = servers
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
flat_models = get_flat_models_from_routes(routes)
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
definitions = get_model_definitions(
|
||||
@@ -391,7 +416,9 @@ def get_openapi(
|
||||
)
|
||||
for route in routes:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(route=route, model_name_map=model_name_map)
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
|
||||
@@ -34,7 +34,7 @@ from fastapi.types import DecoratedCallable
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_response_field,
|
||||
generate_operation_id_for_path,
|
||||
generate_unique_id,
|
||||
get_value_or_default,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
@@ -335,21 +335,47 @@ class APIRoute(routing.Route):
|
||||
dependency_overrides_provider: Optional[Any] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Union[
|
||||
Callable[["APIRoute"], str], DefaultPlaceholder
|
||||
] = Default(generate_unique_id),
|
||||
) -> None:
|
||||
# normalise enums e.g. http.HTTPStatus
|
||||
if isinstance(status_code, IntEnum):
|
||||
status_code = int(status_code)
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
self.response_model = response_model
|
||||
self.summary = summary
|
||||
self.response_description = response_description
|
||||
self.deprecated = deprecated
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
self.response_model_by_alias = response_model_by_alias
|
||||
self.response_model_exclude_unset = response_model_exclude_unset
|
||||
self.response_model_exclude_defaults = response_model_exclude_defaults
|
||||
self.response_model_exclude_none = response_model_exclude_none
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.callbacks = callbacks
|
||||
self.openapi_extra = openapi_extra
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
self.tags = tags or []
|
||||
self.responses = responses or {}
|
||||
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[str] = 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 isinstance(generate_unique_id_function, DefaultPlaceholder):
|
||||
current_generate_unique_id: Callable[
|
||||
["APIRoute"], str
|
||||
] = generate_unique_id_function.value
|
||||
else:
|
||||
current_generate_unique_id = generate_unique_id_function
|
||||
self.unique_id = self.operation_id or current_generate_unique_id(self)
|
||||
# normalize enums e.g. http.HTTPStatus
|
||||
if isinstance(status_code, IntEnum):
|
||||
status_code = int(status_code)
|
||||
self.status_code = status_code
|
||||
if self.response_model:
|
||||
assert (
|
||||
status_code not in STATUS_CODES_WITH_NO_BODY
|
||||
@@ -371,19 +397,14 @@ class APIRoute(routing.Route):
|
||||
else:
|
||||
self.response_field = None # type: ignore
|
||||
self.secure_cloned_response_field = None
|
||||
self.status_code = status_code
|
||||
self.tags = tags or []
|
||||
if dependencies:
|
||||
self.dependencies = list(dependencies)
|
||||
else:
|
||||
self.dependencies = []
|
||||
self.summary = summary
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
# truncate description text to the content preceding the first "form feed"
|
||||
self.description = self.description.split("\f")[0]
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
for additional_status_code, response in self.responses.items():
|
||||
assert isinstance(response, dict), "An additional response must be a dict"
|
||||
@@ -399,16 +420,6 @@ class APIRoute(routing.Route):
|
||||
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
|
||||
else:
|
||||
self.response_fields = {}
|
||||
self.deprecated = deprecated
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
self.response_model_by_alias = response_model_by_alias
|
||||
self.response_model_exclude_unset = response_model_exclude_unset
|
||||
self.response_model_exclude_defaults = response_model_exclude_defaults
|
||||
self.response_model_exclude_none = response_model_exclude_none
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
|
||||
assert callable(endpoint), "An endpoint must be a callable"
|
||||
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
|
||||
@@ -418,10 +429,7 @@ class APIRoute(routing.Route):
|
||||
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.callbacks = callbacks
|
||||
self.app = request_response(self.get_route_handler())
|
||||
self.openapi_extra = openapi_extra
|
||||
|
||||
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
return get_request_handler(
|
||||
@@ -465,6 +473,9 @@ class APIRouter(routing.Router):
|
||||
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
super().__init__(
|
||||
routes=routes, # type: ignore # in Starlette
|
||||
@@ -488,6 +499,7 @@ class APIRouter(routing.Router):
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
self.generate_unique_id_function = generate_unique_id_function
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
@@ -519,6 +531,9 @@ class APIRouter(routing.Router):
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Union[
|
||||
Callable[[APIRoute], str], DefaultPlaceholder
|
||||
] = Default(generate_unique_id),
|
||||
) -> None:
|
||||
route_class = route_class_override or self.route_class
|
||||
responses = responses or {}
|
||||
@@ -535,6 +550,9 @@ class APIRouter(routing.Router):
|
||||
current_callbacks = self.callbacks.copy()
|
||||
if callbacks:
|
||||
current_callbacks.extend(callbacks)
|
||||
current_generate_unique_id = get_value_or_default(
|
||||
generate_unique_id_function, self.generate_unique_id_function
|
||||
)
|
||||
route = route_class(
|
||||
self.prefix + path,
|
||||
endpoint=endpoint,
|
||||
@@ -561,6 +579,7 @@ class APIRouter(routing.Router):
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -590,6 +609,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
def decorator(func: DecoratedCallable) -> DecoratedCallable:
|
||||
self.add_api_route(
|
||||
@@ -617,6 +639,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
return func
|
||||
|
||||
@@ -654,6 +677,9 @@ class APIRouter(routing.Router):
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
include_in_schema: bool = True,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -694,6 +720,12 @@ class APIRouter(routing.Router):
|
||||
current_callbacks.extend(callbacks)
|
||||
if route.callbacks:
|
||||
current_callbacks.extend(route.callbacks)
|
||||
current_generate_unique_id = get_value_or_default(
|
||||
route.generate_unique_id_function,
|
||||
router.generate_unique_id_function,
|
||||
generate_unique_id_function,
|
||||
self.generate_unique_id_function,
|
||||
)
|
||||
self.add_api_route(
|
||||
prefix + route.path,
|
||||
route.endpoint,
|
||||
@@ -722,6 +754,7 @@ class APIRouter(routing.Router):
|
||||
route_class_override=type(route),
|
||||
callbacks=current_callbacks,
|
||||
openapi_extra=route.openapi_extra,
|
||||
generate_unique_id_function=current_generate_unique_id,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
methods = list(route.methods or []) # type: ignore # in Starlette
|
||||
@@ -770,6 +803,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -795,6 +831,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -822,6 +859,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -847,6 +887,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -874,6 +915,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -899,6 +943,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -926,6 +971,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -951,6 +999,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -978,6 +1027,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -1003,6 +1055,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -1030,6 +1083,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -1055,6 +1111,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -1082,6 +1139,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
return self.api_route(
|
||||
path=path,
|
||||
@@ -1107,6 +1167,7 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -1134,6 +1195,9 @@ class APIRouter(routing.Router):
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[BaseRoute]] = None,
|
||||
openapi_extra: Optional[Dict[str, Any]] = None,
|
||||
generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
||||
generate_unique_id
|
||||
),
|
||||
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
|
||||
return self.api_route(
|
||||
@@ -1160,4 +1224,5 @@ class APIRouter(routing.Router):
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
openapi_extra=openapi_extra,
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, Set, Type, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
|
||||
|
||||
import fastapi
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
@@ -13,6 +14,9 @@ from pydantic.fields import FieldInfo, ModelField, UndefinedType
|
||||
from pydantic.schema import model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
@@ -119,13 +123,29 @@ def create_cloned_field(
|
||||
return new_field
|
||||
|
||||
|
||||
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
|
||||
def generate_operation_id_for_path(
|
||||
*, name: str, path: str, method: str
|
||||
) -> str: # pragma: nocover
|
||||
warnings.warn(
|
||||
"fastapi.utils.generate_operation_id_for_path() was deprecated, "
|
||||
"it is not used internally, and will be removed soon",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
operation_id = name + path
|
||||
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
|
||||
|
||||
def generate_unique_id(route: "APIRoute") -> str:
|
||||
operation_id = route.name + route.path_format
|
||||
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
|
||||
assert route.methods
|
||||
operation_id = operation_id + "_" + list(route.methods)[0].lower()
|
||||
return operation_id
|
||||
|
||||
|
||||
def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None:
|
||||
for key in update_dict:
|
||||
if (
|
||||
|
||||
1617
tests/test_generate_unique_id_function.py
Normal file
@@ -1,3 +1,5 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, Depends, FastAPI, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -343,7 +345,11 @@ client = TestClient(app)
|
||||
|
||||
def test_openapi():
|
||||
client = TestClient(app)
|
||||
response = client.get("/openapi.json")
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
response = client.get("/openapi.json")
|
||||
assert issubclass(w[-1].category, UserWarning)
|
||||
assert "Duplicate Operation ID" in str(w[-1].message)
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
|
||||
188
tests/test_tutorial/test_generate_clients/test_tutorial003.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.generate_clients.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"tags": ["items"],
|
||||
"summary": "Get Items",
|
||||
"operationId": "items-get_items",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response Items-Get Items",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"post": {
|
||||
"tags": ["items"],
|
||||
"summary": "Create Item",
|
||||
"operationId": "items-create_item",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseMessage"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/users/": {
|
||||
"post": {
|
||||
"tags": ["users"],
|
||||
"summary": "Create User",
|
||||
"operationId": "users-create_user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/User"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResponseMessage"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "price"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
},
|
||||
},
|
||||
"ResponseMessage": {
|
||||
"title": "ResponseMessage",
|
||||
"required": ["message"],
|
||||
"type": "object",
|
||||
"properties": {"message": {"title": "Message", "type": "string"}},
|
||||
},
|
||||
"User": {
|
||||
"title": "User",
|
||||
"required": ["username", "email"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"email": {"title": "Email", "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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi():
|
||||
with client:
|
||||
response = client.get("/openapi.json")
|
||||
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_items():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 5})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "Item received"}
|
||||
|
||||
|
||||
def test_post_users():
|
||||
response = client.post(
|
||||
"/users/", json={"username": "Foo", "email": "foo@example.com"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "User received"}
|
||||
|
||||
|
||||
def test_get_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [
|
||||
{"name": "Plumbus", "price": 3},
|
||||
{"name": "Portal Gun", "price": 9001},
|
||||
]
|
||||