Skip to main content

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

multipart_response-0.1.0.tar.gz (66.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

multipart_response-0.1.0-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

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

Hashes for multipart_response-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9204bed41359f3ad034bf0c3a2daac55cae3c6ba2f6584badb00069b24e32a14
MD5 eface3959ed8b2c542b685631997a7ec
BLAKE2b-256 8784d9728a1e1a650712bb17951109346d6851f715c09ebec5be132d7dfb8d02

See more details on using hashes here.

Provenance

The following attestation bundles were made for multipart_response-0.1.0.tar.gz:

Publisher: release.yml on scriptogre/multipart-response

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file multipart_response-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for multipart_response-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c4d7bd441bc4cde4857668d65b1eff0b29f819cf4b0062e2ba509e24fdd35c3f
MD5 44a5d12e74eb1b970660dc04fcd80859
BLAKE2b-256 0704e19df55f2d8553aa6bc16d24690a605dcda0694a4a362f30d8a0d7e075a9

See more details on using hashes here.

Provenance

The following attestation bundles were made for multipart_response-0.1.0-py3-none-any.whl:

Publisher: release.yml on scriptogre/multipart-response

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page