Skip to content

Commit 7797bd1

Browse files
Document BlackSheep 2.6.0 multipart/form-data improvements
Co-authored-by: RobertoPrevato <2576032+RobertoPrevato@users.noreply.github.com>
1 parent cab048a commit 7797bd1

File tree

2 files changed

+282
-7
lines changed

2 files changed

+282
-7
lines changed

blacksheep/docs/openapi.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,130 @@ components: {}
222222
tags: []
223223
```
224224
225+
### Request body binders support
226+
227+
/// admonition | Enhanced in BlackSheep 2.6.0
228+
type: info
229+
230+
BlackSheep 2.6.0 adds full OpenAPI documentation support for `FromText` and `FromFiles` binders. These binders are now automatically documented with appropriate request body schemas and content types.
231+
232+
///
233+
234+
BlackSheep automatically generates OpenAPI documentation for various request body binders:
235+
236+
#### FromJSON
237+
238+
Documented with `application/json` content type and the appropriate schema:
239+
240+
```python
241+
from dataclasses import dataclass
242+
from blacksheep import FromJSON, post
243+
244+
245+
@dataclass
246+
class CreateUserInput:
247+
name: str
248+
email: str
249+
age: int
250+
251+
252+
@docs(
253+
summary="Create a new user",
254+
responses={201: "User created successfully"}
255+
)
256+
@post("/api/users")
257+
async def create_user(input: FromJSON[CreateUserInput]):
258+
return {"user_id": 123}
259+
```
260+
261+
The OpenAPI documentation automatically includes the request body schema for `CreateUserInput`.
262+
263+
#### FromFiles (since 2.6.0)
264+
265+
Documented with `multipart/form-data` content type:
266+
267+
```python
268+
from blacksheep import FromFiles, post
269+
270+
271+
@docs(
272+
summary="Upload files",
273+
responses={201: "Files uploaded successfully"}
274+
)
275+
@post("/api/upload")
276+
async def upload_files(files: FromFiles):
277+
return {"files_count": len(files.value)}
278+
```
279+
280+
The OpenAPI documentation automatically documents this as a file upload endpoint with `multipart/form-data` encoding.
281+
282+
#### FromText (since 2.6.0)
283+
284+
Documented with `text/plain` content type:
285+
286+
```python
287+
from blacksheep import FromText, post
288+
289+
290+
@docs(
291+
summary="Store text content",
292+
responses={201: "Text stored successfully"}
293+
)
294+
@post("/api/text")
295+
async def store_text(content: FromText):
296+
return {"length": len(content.value)}
297+
```
298+
299+
#### Mixed multipart/form-data (since 2.6.0)
300+
301+
When combining `FromText` and `FromFiles` in the same endpoint, BlackSheep generates appropriate `multipart/form-data` documentation:
302+
303+
```python
304+
from blacksheep import FromFiles, FromText, post
305+
306+
307+
@docs(
308+
summary="Upload files with description",
309+
responses={201: "Upload completed successfully"}
310+
)
311+
@post("/api/upload-with-metadata")
312+
async def upload_with_metadata(
313+
description: FromText,
314+
files: FromFiles,
315+
):
316+
return {
317+
"description": description.value,
318+
"files_count": len(files.value)
319+
}
320+
```
321+
322+
The OpenAPI specification will correctly document both the text field and file upload field as part of the `multipart/form-data` request body.
323+
324+
#### FromForm
325+
326+
Documented with `application/x-www-form-urlencoded` or `multipart/form-data` content type:
327+
328+
```python
329+
from dataclasses import dataclass
330+
from blacksheep import FromForm, post
331+
332+
333+
@dataclass
334+
class ContactForm:
335+
name: str
336+
email: str
337+
message: str
338+
339+
340+
@docs(
341+
summary="Submit contact form",
342+
responses={200: "Form submitted successfully"}
343+
)
344+
@post("/api/contact")
345+
async def submit_contact(form: FromForm[ContactForm]):
346+
return {"status": "received"}
347+
```
348+
225349
### Adding description and summary
226350

227351
An endpoint description can be specified either using a `docstring`:

blacksheep/docs/requests.md

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ kinds.
177177

178178
#### Reading a form request body
179179

180+
/// admonition | Improved in BlackSheep 2.6.0
181+
type: info
182+
183+
Starting from BlackSheep 2.6.0, `request.form()` and `request.multipart()` use `SpooledTemporaryFile` for memory-efficient file handling. Small files (<1MB) are kept in memory, while larger files automatically spill to temporary disk files. The framework automatically cleans up resources at the end of each request.
184+
185+
///
186+
180187
=== "Using binders (recommended)"
181188

182189
```python
@@ -259,35 +266,179 @@ kinds.
259266
# data is bytes
260267
```
261268

262-
#### Reading files
269+
#### Reading files and multipart/form-data
270+
271+
/// admonition | Significantly improved in BlackSheep 2.6.0
272+
type: info
273+
274+
BlackSheep 2.6.0 introduces significant improvements for handling `multipart/form-data` with memory-efficient streaming and file handling:
263275

264-
Files read from `multipart/form-data` payload.
276+
- **Memory-efficient file handling**: Files use `SpooledTemporaryFile` - small files (<1MB) stay in memory, larger files automatically spill to temporary disk files
277+
- **True streaming parsing**: New `Request.multipart_stream()` method for streaming multipart data without buffering the entire request body
278+
- **Automatic resource cleanup**: The framework automatically calls `Request.dispose()` at the end of each request to clean up file resources
279+
- **Better API**: `FileBuffer` class provides clean methods (`read()`, `seek()`, `close()`, `save_to()`) for uploaded files
280+
- **Streaming parts**: `FormPart.stream()` method to stream part data in chunks
281+
- **OpenAPI support**: `FromText` and `FromFiles` are now properly documented in OpenAPI
282+
283+
///
284+
285+
Files are read from `multipart/form-data` payload.
265286

266287
=== "Using binders (recommended)"
267288

268289
```python
269-
from blacksheep import FromFiles
290+
from blacksheep import FromFiles, post
270291

271292

272-
@post("/something")
293+
@post("/upload")
273294
async def post_files(files: FromFiles):
274-
data = files.value
295+
# files.value is a list of FormPart objects
296+
for file_part in files.value:
297+
# Access file metadata
298+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
299+
content_type = file_part.content_type.decode() if file_part.content_type else None
300+
301+
# file_part.file is a FileBuffer instance with efficient memory handling
302+
# Small files (<1MB) are kept in memory, larger files use temporary disk files
303+
file_buffer = file_part.file
304+
305+
# Read file content
306+
content = file_buffer.read()
307+
308+
# Or save directly to disk
309+
await file_buffer.save_to(f"./uploads/{file_name}")
275310
```
276311

277312
=== "Directly from the request"
278313

279314
```python
315+
from blacksheep import post, Request
316+
317+
280318
@post("/upload-files")
281319
async def upload_files(request: Request):
282320
files = await request.files()
283321

284322
for part in files:
323+
# Access file metadata
324+
file_name = part.file_name.decode() if part.file_name else "unknown"
325+
326+
# file_bytes contains the entire file content
285327
file_bytes = part.data
286-
file_name = file.file_name.decode()
328+
329+
# Or use the FileBuffer for more control
330+
file_buffer = part.file
331+
content = file_buffer.read()
332+
```
287333

288-
...
334+
=== "Memory-efficient streaming (2.6.0+)"
335+
336+
For handling large file uploads efficiently without loading the entire request body into memory:
337+
338+
```python
339+
from blacksheep import post, Request, created
340+
341+
342+
@post("/upload-large")
343+
async def upload_large_files(request: Request):
344+
# Stream multipart data without buffering entire request body
345+
async for part in request.multipart_stream():
346+
if part.file_name:
347+
# This is a file upload
348+
file_name = part.file_name.decode()
349+
350+
# Stream the file content in chunks
351+
with open(f"./uploads/{file_name}", "wb") as f:
352+
async for chunk in part.stream():
353+
f.write(chunk)
354+
else:
355+
# This is a regular form field
356+
field_name = part.name.decode() if part.name else ""
357+
field_value = part.data.decode()
358+
print(f"Field {field_name}: {field_value}")
359+
360+
return created()
289361
```
290362

363+
=== "Mixed form with files and text (2.6.0+)"
364+
365+
Using `FromFiles` and `FromText` together in the same handler:
366+
367+
```python
368+
from blacksheep import FromFiles, FromText, post
369+
370+
371+
@post("/upload-with-description")
372+
async def upload_with_metadata(
373+
description: FromText,
374+
files: FromFiles,
375+
):
376+
# description.value contains the text field value
377+
text_content = description.value
378+
379+
# files.value contains the uploaded files
380+
for file_part in files.value:
381+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
382+
383+
# Process the file
384+
await file_part.file.save_to(f"./uploads/{file_name}")
385+
386+
return {"description": text_content, "files_count": len(files.value)}
387+
```
388+
389+
##### Resource management and cleanup
390+
391+
BlackSheep automatically manages file resources. The framework calls `Request.dispose()` at the end of each request-response cycle to clean up temporary files. However, if you need manual control:
392+
393+
```python
394+
from blacksheep import post, Request
395+
396+
397+
@post("/manual-cleanup")
398+
async def manual_file_handling(request: Request):
399+
try:
400+
files = await request.files()
401+
402+
for part in files:
403+
# Process files
404+
pass
405+
finally:
406+
# Manually clean up resources if needed
407+
# (normally not required as framework does this automatically)
408+
request.dispose()
409+
```
410+
411+
##### FileBuffer API
412+
413+
The `FileBuffer` class wraps `SpooledTemporaryFile` and provides these methods:
414+
415+
- `read(size: int = -1) -> bytes`: Read file content
416+
- `seek(offset: int, whence: int = 0) -> int`: Change file position
417+
- `close() -> None`: Close the file
418+
- `save_to(file_path: str) -> None`: Save file to disk (async)
419+
420+
```python
421+
from blacksheep import FromFiles, post
422+
423+
424+
@post("/process-file")
425+
async def process_file(files: FromFiles):
426+
for file_part in files.value:
427+
file_buffer = file_part.file
428+
429+
# Read first 100 bytes
430+
header = file_buffer.read(100)
431+
432+
# Go back to start
433+
file_buffer.seek(0)
434+
435+
# Read entire content
436+
full_content = file_buffer.read()
437+
438+
# Save to disk
439+
await file_buffer.save_to("./output.bin")
440+
```
441+
291442
#### Reading streams
292443

293444
Reading streams enables reading large-sized bodies using an asynchronous

0 commit comments

Comments
 (0)