mirror of https://github.com/tiangolo/fastapi.git
✨ Add support for declaring `UploadFile` parameters without explicit `File()` (#4469)
This commit is contained in:
parent
59b1f353b3
commit
1bf55200a9
|
|
@ -17,7 +17,7 @@ Import `File` and `UploadFile` from `fastapi`:
|
|||
{!../../../docs_src/request_files/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Define `File` parameters
|
||||
## Define `File` Parameters
|
||||
|
||||
Create file parameters the same way you would for `Body` or `Form`:
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ Have in mind that this means that the whole contents will be stored in memory. T
|
|||
|
||||
But there are several cases in which you might benefit from using `UploadFile`.
|
||||
|
||||
## `File` parameters with `UploadFile`
|
||||
## `File` Parameters with `UploadFile`
|
||||
|
||||
Define a `File` parameter with a type of `UploadFile`:
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ Define a `File` parameter with a type of `UploadFile`:
|
|||
|
||||
Using `UploadFile` has several advantages over `bytes`:
|
||||
|
||||
* You don't have to use `File()` in the default value.
|
||||
* It uses a "spooled" file:
|
||||
* A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk.
|
||||
* This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory.
|
||||
|
|
@ -113,7 +114,31 @@ The way HTML forms (`<form></form>`) sends the data to the server normally uses
|
|||
|
||||
This is not a limitation of **FastAPI**, it's part of the HTTP protocol.
|
||||
|
||||
## Multiple file uploads
|
||||
## Optional File Upload
|
||||
|
||||
You can make a file optional by using standard type annotations:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="9 17"
|
||||
{!> ../../../docs_src/request_files/tutorial001_02.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="7 14"
|
||||
{!> ../../../docs_src/request_files/tutorial001_02_py310.py!}
|
||||
```
|
||||
|
||||
## `UploadFile` with Additional Metadata
|
||||
|
||||
You can also use `File()` with `UploadFile` to set additional parameters in `File()`, for example additional metadata:
|
||||
|
||||
```Python hl_lines="13"
|
||||
{!../../../docs_src/request_files/tutorial001_03.py!}
|
||||
```
|
||||
|
||||
## Multiple File Uploads
|
||||
|
||||
It's possible to upload several files at the same time.
|
||||
|
||||
|
|
@ -140,6 +165,22 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s.
|
|||
|
||||
**FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette.
|
||||
|
||||
### Multiple File Uploads with Additional Metadata
|
||||
|
||||
And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`:
|
||||
|
||||
=== "Python 3.6 and above"
|
||||
|
||||
```Python hl_lines="18"
|
||||
{!> ../../../docs_src/request_files/tutorial003.py!}
|
||||
```
|
||||
|
||||
=== "Python 3.9 and above"
|
||||
|
||||
```Python hl_lines="16"
|
||||
{!> ../../../docs_src/request_files/tutorial003_py39.py!}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
Use `File` to declare files to be uploaded as input parameters (as form data).
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ async def create_file(file: bytes = File(...)):
|
|||
|
||||
|
||||
@app.post("/uploadfile/")
|
||||
async def create_upload_file(file: UploadFile = File(...)):
|
||||
async def create_upload_file(file: UploadFile):
|
||||
return {"filename": file.filename}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_file(file: Optional[bytes] = File(None)):
|
||||
if not file:
|
||||
return {"message": "No file sent"}
|
||||
else:
|
||||
return {"file_size": len(file)}
|
||||
|
||||
|
||||
@app.post("/uploadfile/")
|
||||
async def create_upload_file(file: Optional[UploadFile] = None):
|
||||
if not file:
|
||||
return {"message": "No upload file sent"}
|
||||
else:
|
||||
return {"filename": file.filename}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from fastapi import FastAPI, File, UploadFile
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_file(file: bytes | None = File(None)):
|
||||
if not file:
|
||||
return {"message": "No file sent"}
|
||||
else:
|
||||
return {"file_size": len(file)}
|
||||
|
||||
|
||||
@app.post("/uploadfile/")
|
||||
async def create_upload_file(file: UploadFile | None = None):
|
||||
if not file:
|
||||
return {"message": "No upload file sent"}
|
||||
else:
|
||||
return {"filename": file.filename}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from fastapi import FastAPI, File, UploadFile
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_file(file: bytes = File(..., description="A file read as bytes")):
|
||||
return {"file_size": len(file)}
|
||||
|
||||
|
||||
@app.post("/uploadfile/")
|
||||
async def create_upload_file(
|
||||
file: UploadFile = File(..., description="A file read as UploadFile")
|
||||
):
|
||||
return {"filename": file.filename}
|
||||
|
|
@ -12,7 +12,7 @@ async def create_files(files: List[bytes] = File(...)):
|
|||
|
||||
|
||||
@app.post("/uploadfiles/")
|
||||
async def create_upload_files(files: List[UploadFile] = File(...)):
|
||||
async def create_upload_files(files: List[UploadFile]):
|
||||
return {"filenames": [file.filename for file in files]}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ async def create_files(files: list[bytes] = File(...)):
|
|||
|
||||
|
||||
@app.post("/uploadfiles/")
|
||||
async def create_upload_files(files: list[UploadFile] = File(...)):
|
||||
async def create_upload_files(files: list[UploadFile]):
|
||||
return {"filenames": [file.filename for file in files]}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_files(
|
||||
files: List[bytes] = File(..., description="Multiple files as bytes")
|
||||
):
|
||||
return {"file_sizes": [len(file) for file in files]}
|
||||
|
||||
|
||||
@app.post("/uploadfiles/")
|
||||
async def create_upload_files(
|
||||
files: List[UploadFile] = File(..., description="Multiple files as UploadFile")
|
||||
):
|
||||
return {"filenames": [file.filename for file in files]}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
content = """
|
||||
<body>
|
||||
<form action="/files/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body>
|
||||
"""
|
||||
return HTMLResponse(content=content)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_files(
|
||||
files: list[bytes] = File(..., description="Multiple files as bytes")
|
||||
):
|
||||
return {"file_sizes": [len(file) for file in files]}
|
||||
|
||||
|
||||
@app.post("/uploadfiles/")
|
||||
async def create_upload_files(
|
||||
files: list[UploadFile] = File(..., description="Multiple files as UploadFile")
|
||||
):
|
||||
return {"filenames": [file.filename for file in files]}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
content = """
|
||||
<body>
|
||||
<form action="/files/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body>
|
||||
"""
|
||||
return HTMLResponse(content=content)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
|
||||
|
||||
from starlette.datastructures import URL as URL # noqa: F401
|
||||
from starlette.datastructures import Address as Address # noqa: F401
|
||||
|
|
@ -20,6 +20,10 @@ class UploadFile(StarletteUploadFile):
|
|||
raise ValueError(f"Expected UploadFile, received: {type(v)}")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -390,6 +390,8 @@ def get_param_field(
|
|||
field.required = required
|
||||
if not had_schema and not is_scalar_field(field=field):
|
||||
field.field_info = params.Body(field_info.default)
|
||||
if not had_schema and lenient_issubclass(field.type_, UploadFile):
|
||||
field.field_info = params.File(field_info.default)
|
||||
|
||||
return field
|
||||
|
||||
|
|
@ -701,25 +703,6 @@ def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
|
|||
return missing_field_error
|
||||
|
||||
|
||||
def get_schema_compatible_field(*, field: ModelField) -> ModelField:
|
||||
out_field = field
|
||||
if lenient_issubclass(field.type_, UploadFile):
|
||||
use_type: type = bytes
|
||||
if field.shape in sequence_shapes:
|
||||
use_type = List[bytes]
|
||||
out_field = create_response_field(
|
||||
name=field.name,
|
||||
type_=use_type,
|
||||
class_validators=field.class_validators,
|
||||
model_config=field.model_config,
|
||||
default=field.default,
|
||||
required=field.required,
|
||||
alias=field.alias,
|
||||
field_info=field.field_info,
|
||||
)
|
||||
return out_field
|
||||
|
||||
|
||||
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
if not flat_dependant.body_params:
|
||||
|
|
@ -729,9 +712,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|||
embed = getattr(field_info, "embed", None)
|
||||
body_param_names_set = {param.name for param in flat_dependant.body_params}
|
||||
if len(body_param_names_set) == 1 and not embed:
|
||||
final_field = get_schema_compatible_field(field=first_param)
|
||||
check_file_field(final_field)
|
||||
return final_field
|
||||
check_file_field(first_param)
|
||||
return first_param
|
||||
# If one field requires to embed, all have to be embedded
|
||||
# in case a sub-dependency is evaluated with a single unique body field
|
||||
# That is combined (embedded) with other body fields
|
||||
|
|
@ -740,7 +722,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|||
model_name = "Body_" + name
|
||||
BodyModel: Type[BaseModel] = create_model(model_name)
|
||||
for f in flat_dependant.body_params:
|
||||
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
|
||||
BodyModel.__fields__[f.name] = f
|
||||
required = any(True for f in flat_dependant.body_params if f.required)
|
||||
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.request_files.tutorial001_02 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"summary": "Create File",
|
||||
"operationId": "create_file_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_file_files__post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/uploadfile/": {
|
||||
"post": {
|
||||
"summary": "Create Upload File",
|
||||
"operationId": "create_upload_file_uploadfile__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file_files__post": {
|
||||
"title": "Body_create_file_files__post",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_file_uploadfile__post": {
|
||||
"title": "Body_create_upload_file_uploadfile__post",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_form_no_body():
|
||||
response = client.post("/files/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "No file sent"}
|
||||
|
||||
|
||||
def test_post_uploadfile_no_body():
|
||||
response = client.post("/uploadfile/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "No upload file sent"}
|
||||
|
||||
|
||||
def test_post_file(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/files/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 14}
|
||||
|
||||
|
||||
def test_post_upload_file(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/uploadfile/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"filename": "test.txt"}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"summary": "Create File",
|
||||
"operationId": "create_file_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_file_files__post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/uploadfile/": {
|
||||
"post": {
|
||||
"summary": "Create Upload File",
|
||||
"operationId": "create_upload_file_uploadfile__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file_files__post": {
|
||||
"title": "Body_create_file_files__post",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_file_uploadfile__post": {
|
||||
"title": "Body_create_upload_file_uploadfile__post",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.request_files.tutorial001_02_py310 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_form_no_body(client: TestClient):
|
||||
response = client.post("/files/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "No file sent"}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_uploadfile_no_body(client: TestClient):
|
||||
response = client.post("/uploadfile/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "No upload file sent"}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_file(tmp_path: Path, client: TestClient):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/files/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 14}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_upload_file(tmp_path: Path, client: TestClient):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/uploadfile/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"filename": "test.txt"}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.request_files.tutorial001_03 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"summary": "Create File",
|
||||
"operationId": "create_file_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_file_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/uploadfile/": {
|
||||
"post": {
|
||||
"summary": "Create Upload File",
|
||||
"operationId": "create_upload_file_uploadfile__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file_files__post": {
|
||||
"title": "Body_create_file_files__post",
|
||||
"required": ["file"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"title": "File",
|
||||
"type": "string",
|
||||
"description": "A file read as bytes",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_file_uploadfile__post": {
|
||||
"title": "Body_create_upload_file_uploadfile__post",
|
||||
"required": ["file"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"title": "File",
|
||||
"type": "string",
|
||||
"description": "A file read as UploadFile",
|
||||
"format": "binary",
|
||||
}
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_file(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/files/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 14}
|
||||
|
||||
|
||||
def test_post_upload_file(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file:
|
||||
response = client.post("/uploadfile/", files={"file": file})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"filename": "test.txt"}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.request_files.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"summary": "Create Files",
|
||||
"operationId": "create_files_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_files_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/uploadfiles/": {
|
||||
"post": {
|
||||
"summary": "Create Upload Files",
|
||||
"operationId": "create_upload_files_uploadfiles__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"get": {
|
||||
"summary": "Main",
|
||||
"operationId": "main__get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_files_files__post": {
|
||||
"title": "Body_create_files_files__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as bytes",
|
||||
}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_files_uploadfiles__post": {
|
||||
"title": "Body_create_upload_files_uploadfiles__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as UploadFile",
|
||||
}
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_files(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
path2 = tmp_path / "test2.txt"
|
||||
path2.write_bytes(b"<file content2>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file, path2.open("rb") as file2:
|
||||
response = client.post(
|
||||
"/files/",
|
||||
files=(
|
||||
("files", ("test.txt", file)),
|
||||
("files", ("test2.txt", file2)),
|
||||
),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_sizes": [14, 15]}
|
||||
|
||||
|
||||
def test_post_upload_file(tmp_path):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
path2 = tmp_path / "test2.txt"
|
||||
path2.write_bytes(b"<file content2>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file, path2.open("rb") as file2:
|
||||
response = client.post(
|
||||
"/uploadfiles/",
|
||||
files=(
|
||||
("files", ("test.txt", file)),
|
||||
("files", ("test2.txt", file2)),
|
||||
),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"filenames": ["test.txt", "test2.txt"]}
|
||||
|
||||
|
||||
def test_get_root():
|
||||
client = TestClient(app)
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert b"<form" in response.content
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"summary": "Create Files",
|
||||
"operationId": "create_files_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_files_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/uploadfiles/": {
|
||||
"post": {
|
||||
"summary": "Create Upload Files",
|
||||
"operationId": "create_upload_files_uploadfiles__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"get": {
|
||||
"summary": "Main",
|
||||
"operationId": "main__get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_files_files__post": {
|
||||
"title": "Body_create_files_files__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as bytes",
|
||||
}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_files_uploadfiles__post": {
|
||||
"title": "Body_create_upload_files_uploadfiles__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"description": "Multiple files as UploadFile",
|
||||
}
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="app")
|
||||
def get_app():
|
||||
from docs_src.request_files.tutorial003_py39 import app
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client(app: FastAPI):
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
file_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "files"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_files(tmp_path, app: FastAPI):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
path2 = tmp_path / "test2.txt"
|
||||
path2.write_bytes(b"<file content2>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file, path2.open("rb") as file2:
|
||||
response = client.post(
|
||||
"/files/",
|
||||
files=(
|
||||
("files", ("test.txt", file)),
|
||||
("files", ("test2.txt", file2)),
|
||||
),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_sizes": [14, 15]}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_upload_file(tmp_path, app: FastAPI):
|
||||
path = tmp_path / "test.txt"
|
||||
path.write_bytes(b"<file content>")
|
||||
path2 = tmp_path / "test2.txt"
|
||||
path2.write_bytes(b"<file content2>")
|
||||
|
||||
client = TestClient(app)
|
||||
with path.open("rb") as file, path2.open("rb") as file2:
|
||||
response = client.post(
|
||||
"/uploadfiles/",
|
||||
files=(
|
||||
("files", ("test.txt", file)),
|
||||
("files", ("test2.txt", file2)),
|
||||
),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"filenames": ["test.txt", "test2.txt"]}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_get_root(app: FastAPI):
|
||||
client = TestClient(app)
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert b"<form" in response.content
|
||||
Loading…
Reference in New Issue