mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-14 13:36:21 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83b1a117cc | |||
| 2a1ff213a0 | |||
| 62af6e0eeb | |||
| 15da01af5c | |||
| d544bdf092 | |||
| 703ade7967 | |||
| 58f135ba2f | |||
| 713d374484 | |||
| 24e9ea28d3 | |||
| cae53138b2 | |||
| a49d45eaa9 |
@@ -1,5 +1,26 @@
|
||||
## Next release
|
||||
|
||||
## 0.26.0
|
||||
|
||||
* Separate error handling for validation errors.
|
||||
* This will allow developers to customize the exception handlers.
|
||||
* Document better how to handle exceptions and use error handlers.
|
||||
* Include `RequestValidationError` and `WebSocketRequestValidationError` (this last one will be useful once [encode/starlette#527](https://github.com/encode/starlette/pull/527) or equivalent is merged).
|
||||
* New documentation about exceptions handlers:
|
||||
* [Install custom exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers).
|
||||
* [Override the default exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#override-the-default-exception-handlers).
|
||||
* [Re-use **FastAPI's** exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#re-use-fastapis-exception-handlers).
|
||||
* PR [#273](https://github.com/tiangolo/fastapi/pull/273).
|
||||
|
||||
* Fix support for *paths* in *path parameters* without needing explicit `Path(...)`.
|
||||
* PR [#256](https://github.com/tiangolo/fastapi/pull/256).
|
||||
* Documented in PR [#272](https://github.com/tiangolo/fastapi/pull/272) by [@wshayes](https://github.com/wshayes).
|
||||
* New documentation at: [Path Parameters containing paths](https://fastapi.tiangolo.com/tutorial/path-params/#path-parameters-containing-paths).
|
||||
|
||||
* Update docs for testing FastAPI. Include using `POST`, sending JSON, testing headers, etc. New documentation: [Testing](https://fastapi.tiangolo.com/tutorial/testing/#testing-extended-example). PR [#271](https://github.com/tiangolo/fastapi/pull/271).
|
||||
|
||||
* Fix type declaration of `response_model` to allow generic Python types as `List[Model]`. Mainly to fix `mypy` for users. PR [#266](https://github.com/tiangolo/fastapi/pull/266).
|
||||
|
||||
## 0.25.0
|
||||
|
||||
* Add support for Pydantic's `include`, `exclude`, `by_alias`.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from fastapi import FastAPI, Header, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
fake_secret_token = "coneofsilence"
|
||||
|
||||
fake_db = {
|
||||
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
|
||||
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
description: str = None
|
||||
|
||||
|
||||
@app.get("/items/{item_id}", response_model=Item)
|
||||
async def read_main(item_id: str, x_token: str = Header(...)):
|
||||
if x_token != fake_secret_token:
|
||||
raise HTTPException(status_code=400, detail="Invalid X-Token header")
|
||||
if item_id not in fake_db:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return fake_db[item_id]
|
||||
|
||||
|
||||
@app.post("/items/", response_model=Item)
|
||||
async def create_item(item: Item, x_token: str = Header(...)):
|
||||
if x_token != fake_secret_token:
|
||||
raise HTTPException(status_code=400, detail="Invalid X-Token header")
|
||||
if item.id in fake_db:
|
||||
raise HTTPException(status_code=400, detail="Item already exists")
|
||||
fake_db[item.id] = item
|
||||
return item
|
||||
@@ -0,0 +1,65 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from .main_b import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_read_item():
|
||||
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": "foo",
|
||||
"title": "Foo",
|
||||
"description": "There goes my hero",
|
||||
}
|
||||
|
||||
|
||||
def test_read_item_bad_token():
|
||||
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid X-Token header"}
|
||||
|
||||
|
||||
def test_read_inexistent_item():
|
||||
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
def test_create_item():
|
||||
response = client.post(
|
||||
"/items/",
|
||||
headers={"X-Token": "coneofsilence"},
|
||||
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": "foobar",
|
||||
"title": "Foo Bar",
|
||||
"description": "The Foo Barters",
|
||||
}
|
||||
|
||||
|
||||
def test_create_item_bad_token():
|
||||
response = client.post(
|
||||
"/items/",
|
||||
headers={"X-Token": "hailhydra"},
|
||||
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Invalid X-Token header"}
|
||||
|
||||
|
||||
def test_create_existing_token():
|
||||
response = client.post(
|
||||
"/items/",
|
||||
headers={"X-Token": "coneofsilence"},
|
||||
json={
|
||||
"id": "foo",
|
||||
"title": "The Foo ID Stealers",
|
||||
"description": "There goes my stealer",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Item already exists"}
|
||||
@@ -1,15 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
class UnicornException(Exception):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception(request, exc):
|
||||
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
|
||||
@app.exception_handler(UnicornException)
|
||||
async def unicorn_exception_handler(request: Request, exc: UnicornException):
|
||||
return JSONResponse(
|
||||
status_code=418,
|
||||
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
@app.get("/unicorns/{name}")
|
||||
async def read_unicorn(name: str):
|
||||
if name == "yolo":
|
||||
raise UnicornException(name=name)
|
||||
return {"unicorn_name": name}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request, exc):
|
||||
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request, exc):
|
||||
return PlainTextResponse(str(exc), status_code=400)
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
if item_id == 3:
|
||||
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
|
||||
return {"item_id": item_id}
|
||||
@@ -0,0 +1,28 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
)
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def custom_http_exception_handler(request, exc):
|
||||
print(f"OMG! An HTTP error!: {exc}")
|
||||
return await http_exception_handler(request, exc)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request, exc):
|
||||
print(f"OMG! The client sent invalid data!: {exc}")
|
||||
return await request_validation_exception_handler(request, exc)
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
if item_id == 3:
|
||||
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
|
||||
return {"item_id": item_id}
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/files/{file_path:path}")
|
||||
async def read_user_me(file_path: str):
|
||||
return {"file_path": file_path}
|
||||
@@ -68,7 +68,7 @@ But if the client requests `http://example.com/items/bar` (a non-existent `item_
|
||||
|
||||
They are handled automatically by **FastAPI** and converted to JSON.
|
||||
|
||||
### Adding custom headers
|
||||
## Add custom headers
|
||||
|
||||
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
|
||||
|
||||
@@ -76,24 +76,138 @@ You probably won't need to use it directly in your code.
|
||||
|
||||
But in case you needed it for an advanced scenario, you can add custom headers:
|
||||
|
||||
|
||||
```Python hl_lines="14"
|
||||
{!./src/handling_errors/tutorial002.py!}
|
||||
```
|
||||
|
||||
### Installing custom handlers
|
||||
## Install custom exception handlers
|
||||
|
||||
If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
|
||||
You can add custom exception handlers with <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
|
||||
|
||||
For example, you could override the default exception handler with:
|
||||
Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`.
|
||||
|
||||
```Python hl_lines="2 3 8 9 10"
|
||||
And you want to handle this exception globally with FastAPI.
|
||||
|
||||
You could add a custom exception handler with `@app.exception_handler()`:
|
||||
|
||||
```Python hl_lines="6 7 8 14 15 16 17 18 24"
|
||||
{!./src/handling_errors/tutorial003.py!}
|
||||
```
|
||||
|
||||
...this would make it return "plain text" responses with the errors, instead of JSON responses.
|
||||
Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`.
|
||||
|
||||
!!! info
|
||||
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
|
||||
But it will be handled by the `unicorn_exception_handler`.
|
||||
|
||||
This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be caught by your exception handler.
|
||||
So, you will receive a clean error, with an HTTP status code of `418` and a JSON content of:
|
||||
|
||||
```JSON
|
||||
{"message": "Oops! yolo did something. There goes a rainbow..."}
|
||||
```
|
||||
|
||||
## Override the default exception handlers
|
||||
|
||||
**FastAPI** has some default exception handlers.
|
||||
|
||||
These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data.
|
||||
|
||||
You can override these exception handlers with your own.
|
||||
|
||||
### Override request validation exceptions
|
||||
|
||||
When a request contains invalid data, **FastAPI** internally raises a `RequestValidationError`.
|
||||
|
||||
And it also includes a default exception handler for it.
|
||||
|
||||
To override it, import the `RequestValidationError` and use it with `@app.exception_handler(RequestValidationError)` to decorate the exception handler.
|
||||
|
||||
The exception handler will receive a `Request` and the exception.
|
||||
|
||||
```Python hl_lines="2 14 15 16"
|
||||
{!./src/handling_errors/tutorial004.py!}
|
||||
```
|
||||
|
||||
Now, if you go to `/items/foo`, instead of getting the default JSON error with:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": [
|
||||
"path",
|
||||
"item_id"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
you will get a text version, with:
|
||||
|
||||
```
|
||||
1 validation error
|
||||
path -> item_id
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
```
|
||||
|
||||
#### `RequestValidationError` vs `ValidationError`
|
||||
|
||||
!!! warning
|
||||
These are technical details that you might skip if it's not important for you now.
|
||||
|
||||
`RequestValidationError` is a sub-class of Pydantic's <a href="https://pydantic-docs.helpmanual.io/#error-handling" target="_blank">`ValidationError`</a>.
|
||||
|
||||
**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
|
||||
|
||||
But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with a HTTP status code `500`.
|
||||
|
||||
It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
|
||||
|
||||
And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
|
||||
|
||||
### Override the `HTTPException` error handler
|
||||
|
||||
The same way, you can override the `HTTPException` handler.
|
||||
|
||||
For example, you could want to return a plain text response instead of JSON for these errors:
|
||||
|
||||
```Python hl_lines="1 3 9 10 11 22"
|
||||
{!./src/handling_errors/tutorial004.py!}
|
||||
```
|
||||
|
||||
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
|
||||
|
||||
**FastAPI** has its own `HTTPException`.
|
||||
|
||||
And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class.
|
||||
|
||||
The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response.
|
||||
|
||||
This is needed/used internally for OAuth 2.0 and some security utilities.
|
||||
|
||||
So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code.
|
||||
|
||||
But when you register an exception handler, you should register it for Starlette's `HTTPException`.
|
||||
|
||||
This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises an `HTTPException`, your handler will be able to catch handle it.
|
||||
|
||||
In this example, to be able to have both `HTTPException`s in the same code, Starlette's exceptions is renamed to `StarletteHTTPException`:
|
||||
|
||||
```Python
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
```
|
||||
|
||||
### Re-use **FastAPI**'s exception handlers
|
||||
|
||||
You could also just want to use the exception somehow, but then use the same default exception handlers from **FastAPI**.
|
||||
|
||||
You can import and re-use the default exception handlers from `fastapi.exception_handlers`:
|
||||
|
||||
```Python hl_lines="2 3 4 5 15 21"
|
||||
{!./src/handling_errors/tutorial005.py!}
|
||||
```
|
||||
|
||||
In this example, you are just `print`ing the error with a very expressive notification.
|
||||
|
||||
But you get the idea, you can use the exception and then just re-use the default exception handlers.
|
||||
|
||||
@@ -115,6 +115,42 @@ Because path operations are evaluated in order, you need to make sure that the p
|
||||
|
||||
Otherwise, the path for `/users/{user_id}` would match also for `/users/me`, "thinking" that it's receiving a parameter `user_id` with a value of `"me"`.
|
||||
|
||||
## Path parameters containing paths
|
||||
|
||||
Let's say you have a *path operation* with a path `/files/{file_path}`.
|
||||
|
||||
But you need `file_path` itself to contain a *path*, like `home/johndoe/myfile.txt`.
|
||||
|
||||
So, the URL for that file would be something like: `/files/home/johndoe/myfile.txt`.
|
||||
|
||||
### OpenAPI support
|
||||
|
||||
OpenAPI doesn't support a way to declare a *path parameter* to contain a *path* inside, as that could lead to scenarios that are difficult to test and define.
|
||||
|
||||
Nevertheless, you can still do it in **FastAPI**, using one of the internal tools from Starlette.
|
||||
|
||||
And the docs would still work, although not adding any documentation telling that the parameter should contain a path.
|
||||
|
||||
### Path convertor
|
||||
|
||||
Using an option directly from Starlette you can declare a *path parameter* containing a *path* using a URL like:
|
||||
|
||||
```
|
||||
/files/{file_path:path}
|
||||
```
|
||||
|
||||
In this case, the name of the parameter is `file_path`, and the last part, `:path`, tells it that the parameter should match any *path*.
|
||||
|
||||
So, you can use it with:
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/path_params/tutorial004.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
You could need the parameter to contain `/home/johndoe/myfile.txt`, with a leading slash (`/`).
|
||||
|
||||
In that case, the URL would be: `/files//home/johndoe/myfile.txt`, with a double slash (`//`) between `files` and `home`.
|
||||
|
||||
## Recap
|
||||
|
||||
@@ -127,4 +163,4 @@ With **FastAPI**, by using short, intuitive and standard Python type declaration
|
||||
|
||||
And you only have to declare them once.
|
||||
|
||||
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
|
||||
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
|
||||
|
||||
@@ -22,12 +22,11 @@ Write simple `assert` statements with the standard Python expressions that you n
|
||||
|
||||
!!! tip
|
||||
Notice that the testing functions are normal `def`, not `async def`.
|
||||
|
||||
|
||||
And the calls to the client are also normal calls, not using `await`.
|
||||
|
||||
This allows you to use `pytest` directly without complications.
|
||||
|
||||
|
||||
## Separating tests
|
||||
|
||||
In a real application, you probably would have your tests in a different file.
|
||||
@@ -50,6 +49,51 @@ Then you could have a file `test_main.py` with your tests, and import your `app`
|
||||
{!./src/app_testing/test_main.py!}
|
||||
```
|
||||
|
||||
## Testing: extended example
|
||||
|
||||
Now let's extend this example and add more details to see how to test different parts.
|
||||
|
||||
### Extended **FastAPI** app file
|
||||
|
||||
Let's say you have a file `main_b.py` with your **FastAPI** app.
|
||||
|
||||
It has a `GET` operation that could return an error.
|
||||
|
||||
It has a `POST` operation that could return several errors.
|
||||
|
||||
Both *path operations* require an `X-Token` header.
|
||||
|
||||
```Python
|
||||
{!./src/app_testing/main_b.py!}
|
||||
```
|
||||
|
||||
### Extended testing file
|
||||
|
||||
You could then have a `test_main_b.py`, the same as before, with the extended tests:
|
||||
|
||||
```Python
|
||||
{!./src/app_testing/test_main_b.py!}
|
||||
```
|
||||
|
||||
Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`.
|
||||
|
||||
Then you just do the same in your tests.
|
||||
|
||||
E.g.:
|
||||
|
||||
* To pass a *path* or *query* parameter, add it to the URL itself.
|
||||
* To pass a JSON body, pass a Python object (e.g. a `dict`) to the parameter `json`.
|
||||
* If you need to send *Form Data* instead of JSON, use the `data` parameter instead.
|
||||
* To pass *headers*, use a `dict` in the `headers` parameter.
|
||||
* For *cookies*, a `dict` in the `cookies` parameter.
|
||||
|
||||
For more information about how to pass data to the backend (using `requests` or the `TestClient`) check the <a href="http://docs.python-requests.org" target="_blank">Requests documentation</a>.
|
||||
|
||||
!!! info
|
||||
Note that the `TestClient` receives data that can be converted to JSON, not Pydantic models.
|
||||
|
||||
If you have a Pydantic model in your test and you want to send its data to the application during testing, you can use the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">JSON compatible encoder: `jsonable_encoder`</a>.
|
||||
|
||||
## Testing WebSockets
|
||||
|
||||
You can use the same `TestClient` to test WebSockets.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.25.0"
|
||||
__version__ = "0.26.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
+19
-22
@@ -1,6 +1,11 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
)
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.openapi.docs import (
|
||||
get_redoc_html,
|
||||
get_swagger_ui_html,
|
||||
@@ -8,7 +13,6 @@ from fastapi.openapi.docs import (
|
||||
)
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from pydantic import BaseModel
|
||||
from starlette.applications import Starlette
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
@@ -17,16 +21,6 @@ from starlette.responses import HTMLResponse, JSONResponse, Response
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
headers = getattr(exc, "headers", None)
|
||||
if headers:
|
||||
return JSONResponse(
|
||||
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
|
||||
)
|
||||
else:
|
||||
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
|
||||
|
||||
|
||||
class FastAPI(Starlette):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -120,14 +114,17 @@ class FastAPI(Starlette):
|
||||
)
|
||||
|
||||
self.add_route(self.redoc_url, redoc_html, include_in_schema=False)
|
||||
self.add_exception_handler(HTTPException, http_exception)
|
||||
self.add_exception_handler(HTTPException, http_exception_handler)
|
||||
self.add_exception_handler(
|
||||
RequestValidationError, request_validation_exception_handler
|
||||
)
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
path: str,
|
||||
endpoint: Callable,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -173,7 +170,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -252,7 +249,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -295,7 +292,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -338,7 +335,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -381,7 +378,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -424,7 +421,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -467,7 +464,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -510,7 +507,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
@@ -553,7 +550,7 @@ class FastAPI(Starlette):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
headers = getattr(exc, "headers", None)
|
||||
if headers:
|
||||
return JSONResponse(
|
||||
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
|
||||
)
|
||||
else:
|
||||
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
|
||||
|
||||
|
||||
async def request_validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
from pydantic import ValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
|
||||
@@ -7,3 +8,11 @@ class HTTPException(StarletteHTTPException):
|
||||
) -> None:
|
||||
super().__init__(status_code=status_code, detail=detail)
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
pass
|
||||
|
||||
+18
-22
@@ -13,6 +13,7 @@ from fastapi.dependencies.utils import (
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import Field
|
||||
@@ -28,7 +29,7 @@ from starlette.routing import (
|
||||
request_response,
|
||||
websocket_session,
|
||||
)
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
|
||||
from starlette.status import WS_1008_POLICY_VIOLATION
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
@@ -103,10 +104,7 @@ def get_app(
|
||||
request=request, dependant=dependant, body=body
|
||||
)
|
||||
if errors:
|
||||
errors_out = ValidationError(errors)
|
||||
raise HTTPException(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
|
||||
)
|
||||
raise RequestValidationError(errors)
|
||||
else:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
if is_coroutine:
|
||||
@@ -141,10 +139,7 @@ def get_websocket_app(dependant: Dependant) -> Callable:
|
||||
)
|
||||
if errors:
|
||||
await websocket.close(code=WS_1008_POLICY_VIOLATION)
|
||||
errors_out = ValidationError(errors)
|
||||
raise HTTPException(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
|
||||
)
|
||||
raise WebSocketRequestValidationError(errors)
|
||||
assert dependant.call is not None, "dependant.call must me a function"
|
||||
await dependant.call(**values)
|
||||
|
||||
@@ -169,7 +164,7 @@ class APIRoute(routing.Route):
|
||||
path: str,
|
||||
endpoint: Callable,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -255,10 +250,11 @@ class APIRoute(routing.Route):
|
||||
assert inspect.isfunction(endpoint) or inspect.ismethod(
|
||||
endpoint
|
||||
), f"An endpoint must be a function or method"
|
||||
self.dependant = get_dependant(path=path, call=self.endpoint)
|
||||
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
|
||||
for depends in self.dependencies[::-1]:
|
||||
self.dependant.dependencies.insert(
|
||||
0, get_parameterless_sub_dependant(depends=depends, path=path)
|
||||
0,
|
||||
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
|
||||
self.app = request_response(
|
||||
@@ -282,7 +278,7 @@ class APIRouter(routing.Router):
|
||||
path: str,
|
||||
endpoint: Callable,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -329,7 +325,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -450,7 +446,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -495,7 +491,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -539,7 +535,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -583,7 +579,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -627,7 +623,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -671,7 +667,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -715,7 +711,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
@@ -759,7 +755,7 @@ class APIRouter(routing.Router):
|
||||
self,
|
||||
path: str,
|
||||
*,
|
||||
response_model: Type[BaseModel] = None,
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/unicorns/{name}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Unicorn",
|
||||
"operationId": "read_unicorn_unicorns__name__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Name", "type": "string"},
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"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_get():
|
||||
response = client.get("/unicorns/shinny")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"unicorn_name": "shinny"}
|
||||
|
||||
|
||||
def test_get_exception():
|
||||
response = client.get("/unicorns/yolo")
|
||||
assert response.status_code == 418
|
||||
assert response.json() == {
|
||||
"message": "Oops! yolo did something. There goes a rainbow..."
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial004 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Item",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "integer"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"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_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 400
|
||||
validation_error_str_lines = [
|
||||
b"1 validation error",
|
||||
b"path -> item_id",
|
||||
b" value is not a valid integer (type=type_error.integer)",
|
||||
]
|
||||
assert response.content == b"\n".join(validation_error_str_lines)
|
||||
|
||||
|
||||
def test_get_http_error():
|
||||
response = client.get("/items/3")
|
||||
assert response.status_code == 418
|
||||
assert response.content == b"Nope! I don't like 3."
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/2")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 2}
|
||||
@@ -0,0 +1,103 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial005 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Item",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "integer"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"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_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_get_http_error():
|
||||
response = client.get("/items/3")
|
||||
assert response.status_code == 418
|
||||
assert response.json() == {"detail": "Nope! I don't like 3."}
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/2")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 2}
|
||||
@@ -0,0 +1,91 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_params.tutorial004 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/{file_path}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read User Me",
|
||||
"operationId": "read_user_me_files__file_path__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "File_Path", "type": "string"},
|
||||
"name": "file_path",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"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():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_file_path():
|
||||
response = client.get("/files/home/johndoe/myfile.txt")
|
||||
print(response.content)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_path": "home/johndoe/myfile.txt"}
|
||||
|
||||
|
||||
def test_root_file_path():
|
||||
response = client.get("/files//home/johndoe/myfile.txt")
|
||||
print(response.content)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_path": "/home/johndoe/myfile.txt"}
|
||||
Reference in New Issue
Block a user