Build and stream multipart responses with adapters for Python web frameworks.
Project description
Multipart Response
Stream multipart responses from FastAPI and Starlette.
from fastapi import FastAPI
from multipart_response.fastapi import HTMLMultipartResponse
app = FastAPI()
@app.get("/updates", response_class=HTMLMultipartResponse)
async def updates():
yield "<p>Ready</p>"
yield "<p>Done</p>"
Install
Choose an adapter:
uv add "multipart-response[fastapi]"
uv add "multipart-response[starlette]"
Stream from FastAPI
With FastAPI 0.134.0 or later, yield each part from the path operation:
from fastapi import FastAPI
from multipart_response.fastapi import JSONPart, MultipartResponse, TextPart
app = FastAPI()
@app.get("/report", response_class=MultipartResponse)
async def report():
yield TextPart("Preparing report")
yield JSONPart({"status": "ready"})
The server sends each part as it arrives:
Content-Type: multipart/mixed; boundary=multipart-...
--multipart-...
content-length: 16
content-type: text/plain; charset=utf-8
Preparing report
--multipart-...
content-length: 18
content-type: application/json
{"status":"ready"}
--multipart-...--
Return MultipartResponse directly when you need to set the status or outer headers:
@app.get("/summary")
async def summary() -> MultipartResponse:
return MultipartResponse(
[
TextPart("Summary"),
JSONPart({"complete": True}),
],
status_code=200,
headers={"X-Report": "summary"},
)
Target each htmx swap
Yield (content, headers) to set headers on one HTML part:
from multipart_response.fastapi import HTMLMultipartResponse
@app.get("/dashboard", response_class=HTMLMultipartResponse)
async def dashboard():
yield (
"<p>Ready</p>",
{
"HX-Target": "#status",
"HX-Swap": "innerHTML",
},
)
yield (
"<li>New report</li>",
{
"HX-Target": "#reports",
"HX-Swap": "beforeend",
},
)
The first part replaces #status. The second appends to #reports.
Stream one part
Pass a sync or async chunk source to Part:
from collections.abc import AsyncIterator
from multipart_response.fastapi import MultipartResponse, Part
async def video_chunks() -> AsyncIterator[bytes]:
yield b"first chunk"
yield b"second chunk"
@app.get("/video", response_class=MultipartResponse)
async def video():
yield Part(video_chunks(), media_type="video/mp4")
A streamed part has no automatic Content-Length. Text parts may yield strings, which use the part's charset.
Nest multipart content
Yield Multipart to nest parts with their own subtype and boundary:
from multipart_response.fastapi import HTMLPart, Multipart, MultipartResponse, TextPart
@app.get("/versions", response_class=MultipartResponse)
async def versions():
yield Multipart(
[
TextPart("Plain text"),
HTMLPart("<p>HTML</p>"),
],
subtype="alternative",
)
Wrap the nested entity in Part to add headers:
yield Part(
Multipart(
[
TextPart("Plain text"),
HTMLPart("<p>HTML</p>"),
],
subtype="alternative",
),
headers={"Content-ID": "<versions>"},
)
Part sets the nested Content-Type and boundary. Multipart entities can contain streams and other multipart entities.
Return a Starlette response
Return MultipartResponse from a Starlette endpoint:
from multipart_response.starlette import HTMLPart, JSONPart, MultipartResponse
async def endpoint(request):
return MultipartResponse([
JSONPart({"title": "Example"}),
HTMLPart("<h1>Example</h1>"),
])
Pass a sequence to buffer the body and set its outer Content-Length. Pass a sync or async iterable to stream it.
MultipartResponse requires explicit Part, MultipartPart, or Multipart values. HTMLMultipartResponse also accepts HTML strings and (HTML, headers) pairs. It passes explicit parts through unchanged:
from multipart_response.starlette import HTMLMultipartResponse, JSONPart, TextPart
return HTMLMultipartResponse([
"<p>HTML shorthand</p>",
TextPart("Explicit plain text"),
JSONPart({"status": "ready"}),
])
Build a custom part
Set the media type and headers on Part:
from multipart_response.starlette import Part
part = Part(
b"image data",
media_type="image/png",
headers={"Content-ID": "<preview>"},
)
part.headers["Content-Disposition"] = 'attachment; filename="preview.png"'
part.set_cookie("preview", "ready")
Part adds Content-Length to static bodies. For text media types, it also adds charset=utf-8 when no charset is set.
Part API
| Name | Use |
|---|---|
body |
Read the rendered body or stream source. |
raw_headers |
Read the encoded (name, value) pairs. |
headers |
Read or change headers through Starlette's MutableHeaders. |
media_type |
Set the generated Content-Type. |
charset |
Set the text encoding. Defaults to utf-8. |
render(content) |
Render static content or keep a body stream. Override it in a subclass. |
render_chunk(chunk) |
Encode one string or bytes-like stream chunk. |
init_headers(headers) |
Build raw_headers. Override it in a subclass. |
render_headers() |
Render the header block with CRLF line endings. |
set_cookie(...) |
Add a Set-Cookie part header with Starlette's API. |
delete_cookie(...) |
Expire a cookie in the part headers. |
as_multipart_part() |
Convert to the framework-neutral MultipartPart. |
Set the HTTP status, outer cookies, and background task on MultipartResponse. A part has none of those values.
Use the core writer
Import the dependency-free core to build static, streamed, or nested entities:
from multipart_response import Multipart, MultipartPart
multipart = Multipart(
[
MultipartPart(
b"hello",
[(b"Content-Type", b"text/plain; charset=utf-8")],
),
],
boundary="example",
)
body = multipart.render()
assert multipart.content_type == "multipart/mixed; boundary=example"
Use MultipartWriter directly when you need to call start_part(), write_body(), and finalize() yourself.
Framing rules
The writer:
- Generates a boundary with 128 random bits.
- Limits boundaries to the RFC 2046 grammar and 70-byte maximum.
- Uses CRLF around each delimiter.
- Requires at least one part.
- Rejects invalid header names, values, and line lengths.
- Rejects boundary collisions across body chunks and nesting levels.
- Streams sync and async part bodies.
- Nests multipart entities recursively.
- Keeps header order and duplicate headers.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file multipart_response-0.1.0.tar.gz.
File metadata
- Download URL: multipart_response-0.1.0.tar.gz
- Upload date:
- Size: 66.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9204bed41359f3ad034bf0c3a2daac55cae3c6ba2f6584badb00069b24e32a14
|
|
| MD5 |
eface3959ed8b2c542b685631997a7ec
|
|
| BLAKE2b-256 |
8784d9728a1e1a650712bb17951109346d6851f715c09ebec5be132d7dfb8d02
|
Provenance
The following attestation bundles were made for multipart_response-0.1.0.tar.gz:
Publisher:
release.yml on scriptogre/multipart-response
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
multipart_response-0.1.0.tar.gz -
Subject digest:
9204bed41359f3ad034bf0c3a2daac55cae3c6ba2f6584badb00069b24e32a14 - Sigstore transparency entry: 2191118608
- Sigstore integration time:
-
Permalink:
scriptogre/multipart-response@a400277429ed44c545e27b80e109bc4628930e3d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/scriptogre
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a400277429ed44c545e27b80e109bc4628930e3d -
Trigger Event:
push
-
Statement type:
File details
Details for the file multipart_response-0.1.0-py3-none-any.whl.
File metadata
- Download URL: multipart_response-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4d7bd441bc4cde4857668d65b1eff0b29f819cf4b0062e2ba509e24fdd35c3f
|
|
| MD5 |
44a5d12e74eb1b970660dc04fcd80859
|
|
| BLAKE2b-256 |
0704e19df55f2d8553aa6bc16d24690a605dcda0694a4a362f30d8a0d7e075a9
|
Provenance
The following attestation bundles were made for multipart_response-0.1.0-py3-none-any.whl:
Publisher:
release.yml on scriptogre/multipart-response
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
multipart_response-0.1.0-py3-none-any.whl -
Subject digest:
c4d7bd441bc4cde4857668d65b1eff0b29f819cf4b0062e2ba509e24fdd35c3f - Sigstore transparency entry: 2191118694
- Sigstore integration time:
-
Permalink:
scriptogre/multipart-response@a400277429ed44c545e27b80e109bc4628930e3d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/scriptogre
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a400277429ed44c545e27b80e109bc4628930e3d -
Trigger Event:
push
-
Statement type: