Metadata-Version: 2.4
Name: multipart-response
Version: 0.6.0
Summary: Stream one HTTP response as multiple parts. For Django, FastAPI, FastHTML, and Starlette.
Project-URL: Homepage, https://github.com/scriptogre/multipart-response
Project-URL: Issues, https://github.com/scriptogre/multipart-response/issues
Project-URL: Source, https://github.com/scriptogre/multipart-response
Author-email: Christian Tanul <git@christiantanul.com>
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: django,fastapi,fasthtml,htmx,http,multipart,response,starlette,streaming
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.134.0; extra == 'fastapi'
Provides-Extra: fasthtml
Requires-Dist: python-fasthtml>=0.14.9; extra == 'fasthtml'
Provides-Extra: starlette
Requires-Dist: starlette>=0.37.2; extra == 'starlette'
Description-Content-Type: text/markdown

# multipart-response

Stream one HTTP response as multiple MIME parts. For Django, FastAPI, FastHTML, and Starlette.

## API by adapter

Import from the adapter for your framework:

| Adapter | Public classes | Value handling |
| --- | --- | --- |
| `multipart_response.django` | `MIMEMultipart`, `MultipartResponse`, `Part`, `JsonPart` | Explicit parts. `Part` follows `HttpResponse`; `JsonPart` follows `JsonResponse`. |
| `multipart_response.starlette` | `MIMEMultipart`, `MultipartResponse`, `Part` | Explicit parts with Starlette response semantics. |
| `multipart_response.fastapi` | `MIMEMultipart`, `MultipartResponse`, `Part`, `HTMLMultipartResponse`, `JSONMultipartResponse` | Explicit parts, or homogeneous HTML and JSON values through specialized response classes. |
| `multipart_response.fasthtml` | `MIMEMultipart`, `MultipartResponse`, `Part` | FastHTML components and strings become HTML. Mappings become JSON. |

The package root provides the framework-neutral `MIMEMultipart`, `MIMEPart`, and `MIMEMultipartWriter` classes.

## Response input rules

Every `MultipartResponse` accepts one explicit part or a sequence, sync iterable, or async iterable of parts:

```python
MultipartResponse(Part("one"))
MultipartResponse([
    Part("one"),
    Part("two"),
])
```

An iterable passed to `MultipartResponse` supplies several parts. An iterable passed to `Part` streams one part body:

```python
MultipartResponse([Part("one"), Part("two")])  # Two parts
MultipartResponse((Part("one"), Part("two")))  # Two parts
MultipartResponse(Part(body_chunks()))           # One streamed part
```

Django and Starlette `MultipartResponse` accept only explicit `Part`, `MIMEPart`, or nested `MIMEMultipart` values. FastAPI's generic `MultipartResponse` follows the same rule.

FastAPI's specialized responses add homogeneous value conversion:

```python
HTMLMultipartResponse(["<p>One</p>", "<p>Two</p>"])
JSONMultipartResponse([{"part": 1}, {"part": 2}])
```

`(value, headers)` is shorthand for one part in FastAPI's specialized responses and FastHTML's response:

```python
("<p>Done</p>", {"HX-Target": "#status"})
```

A list or tuple passed to `JSONMultipartResponse` is a source of parts. Wrap a JSON array in the outer source to produce one array part:

```python
JSONMultipartResponse([1, 2])      # Two JSON number parts
JSONMultipartResponse([[1, 2]])    # One JSON array part
```

`content_type` and `media_type` set metadata. They do not choose a serializer.

Django provides `JsonPart` for native JSON serialization. Serialize JSON before passing it to a generic Starlette or FastAPI `Part`:

```python
# Django
JsonPart({"status": "done"})

# Starlette or FastAPI
Part(json.dumps({"status": "done"}), media_type="application/json")
```

FastHTML follows its native response conversion and renders mappings as JSON without `json.dumps()`.

## Integrations

### Django

```console
uv add "multipart-response[django]"
```

Stream parts from a Django view:

```python
from multipart_response.django import JsonPart, MultipartResponse, Part


def generate_report(request):
    def parts():
        yield Part(
            "<p>Generating report...</p>",
            headers={"HX-Target": "#status"},
        )
        yield Part(
            '<li><a href="/reports/42">Quarterly report</a></li>',
            headers={
                "HX-Target": "#reports",
                "HX-Swap": "beforeend",
            },
        )
        yield JsonPart(
            {"report_id": 42, "status": "done"},
            headers={"Content-ID": "result"},
        )

    return MultipartResponse(parts())
```

- `Part(...)` follows Django's [`HttpResponse`](https://docs.djangoproject.com/en/6.0/ref/request-response/#django.http.HttpResponse) API for content, headers, and cookies. HTML is the default content type.
- `JsonPart(...)` follows [`JsonResponse`](https://docs.djangoproject.com/en/6.0/ref/request-response/#jsonresponse-objects) for `encoder`, `safe`, `json_dumps_params`, content type, charset, and headers.
- `MultipartResponse(...)` follows [`StreamingHttpResponse`](https://docs.djangoproject.com/en/6.0/ref/request-response/#django.http.StreamingHttpResponse) and adds `subtype` and `boundary`.
- `content_type="application/json"` on `Part` remains metadata. Use `JsonPart` to serialize a Python value.

Use an async part source under ASGI:

```python
async def updates(request):
    async def parts():
        yield Part("<p>Ready</p>")
        yield Part("<p>Done</p>")

    return MultipartResponse(parts())
```

Django 4.2 or later is required.

*`MultipartResponse` subclasses Django's native [`StreamingHttpResponse`](https://docs.djangoproject.com/en/6.0/ref/request-response/#django.http.StreamingHttpResponse).*

### FastAPI

```console
uv add "multipart-response[fastapi]"
```

Stream multiple content types from one path operation:

```python
import json

from fastapi import FastAPI
from multipart_response.fastapi import MultipartResponse, Part

app = FastAPI()


@app.get("/updates", response_class=MultipartResponse)
async def updates():
    yield Part("<p>Ready</p>", media_type="text/html")
    yield Part(
        json.dumps({"status": "ready"}),
        media_type="application/json",
        headers={"HX-Target": "#status"},
    )
    yield Part(chunk_stream(), media_type="video/mp4")
```

- `Part(...)` follows FastAPI's [`Response`](https://fastapi.tiangolo.com/advanced/custom-response/#response) API for content, media type, headers, and cookies.
- `MultipartResponse(...)` follows [`StreamingResponse`](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse) and adds `subtype` and `boundary`.
- Use [`response_class`](https://fastapi.tiangolo.com/advanced/custom-response/) with yielded parts, or [return the response directly](https://fastapi.tiangolo.com/advanced/response-directly/).

Stream HTML or JSON values directly when every part has one content type:

```python
from multipart_response.fastapi import HTMLMultipartResponse, JSONMultipartResponse


@app.get("/html-updates", response_class=HTMLMultipartResponse)
async def html_updates():
    yield "<p>Ready</p>"
    yield "<p>Done</p>", {"HX-Target": "#status"}


@app.get("/json-updates", response_class=JSONMultipartResponse)
async def json_updates():
    yield {"status": "ready"}, {"HX-Target": "#status"}
    yield "done"
```

`HTMLMultipartResponse` renders strings as HTML. `JSONMultipartResponse` uses FastAPI's [`jsonable_encoder()`](https://fastapi.tiangolo.com/tutorial/encoder/) and sets each part to `application/json`. The final part above contains the JSON string `"done"`, including its quotes.

*`MultipartResponse` subclasses Starlette's native [`StreamingResponse`](https://www.starlette.io/responses/#streamingresponse), which FastAPI uses for streamed responses.*

### FastHTML

```console
uv add "multipart-response[fasthtml]"
```

Return FastHTML values as parts:

```python
from fasthtml.common import Div, P, fast_app
from multipart_response.fasthtml import MultipartResponse, Part

app, rt = fast_app()


@rt
def updates():
    return MultipartResponse([
        P("Ready"),
        {"status": "working"},
        Part(Div("Done"), hx_target="#status"),
    ])
```

FastHTML components and strings become HTML parts. Mappings become JSON parts. Component rendering follows FastHTML's `fh_cfg.indent` setting.

The final part above includes:

```http
Content-Type: text/html; charset=utf-8
HX-Target: #status

<div>Done</div>
```

Stream one component per part:

```python
@rt
async def messages():
    return MultipartResponse(
        P(message) async for message in inbox()
    )
```

Each component produces one part and one htmx swap. Return `MultipartResponse(...)` explicitly. FastHTML does not infer it from a bare async-generator route.

Set htmx response headers on one part:

```python
yield Part(
    P(message.text),
    hx_target="#feed",
    hx_swap="beforeend",
    hx_part_id=str(message.id),
)
```

Any `hx_*` keyword becomes an `HX-*` header. This includes future htmx response headers. Keyword values override matching entries in `headers`. A `None` value does not add or replace a header; an empty string is preserved.

Set envelope defaults for every part:

```python
return MultipartResponse(
    (P(message) async for message in inbox()),
    hx_target="#feed",
    hx_swap="beforeend",
)
```

The response starts with:

```http
Content-Type: multipart/mixed; boundary=...
HX-Target: #feed
HX-Swap: beforeend
```

Part headers override envelope defaults. `MultipartResponse` accepts the same open-ended `hx_*` keywords as `Part`.

Resume a persistent connection after its last completed part:

```python
@rt
async def events(hx_last_part_id: str | None = None):
    async def parts():
        async for message in messages_after(hx_last_part_id):
            yield Part(
                P(message.text),
                hx_target="#feed",
                hx_swap="beforeend",
                hx_part_id=str(message.id),
            )

    return MultipartResponse(parts())
```

FastHTML injects the request's [`HX-Last-Part-ID`](https://four.htmx.org/extensions/hx-multipart#hx-last-part-id) header into the annotated parameter. An empty [`hx_part_id=""`](https://four.htmx.org/extensions/hx-multipart#hx-part-id) resets the reconnect cursor.

Stream several components inside one part:

```python
@rt
async def transcript():
    return MultipartResponse(
        Part(P(message) async for message in inbox())
    )
```

Sync and async component iterables work. Their output forms one part body and one htmx swap.

*`MultipartResponse` subclasses Starlette's native [`StreamingResponse`](https://www.starlette.io/responses/#streamingresponse), which FastHTML uses for streamed responses.*

### Starlette

```console
uv add "multipart-response[starlette]"
```

Return a Starlette response from an endpoint:

```python
from multipart_response.starlette import MultipartResponse, Part


async def updates(request):
    part = Part("Ready", media_type="text/plain", headers={"Content-ID": "status"})
    part.headers["HX-Target"] = "#status"
    part.set_cookie("seen", "yes")

    return MultipartResponse(
        [part],
        status_code=200,
        headers={"X-Stream": "updates"},
    )
```

- `Part(...)` follows Starlette's [`Response`](https://www.starlette.io/responses/#response) API for content, media type, headers, and cookies.
- `MultipartResponse(...)` follows [`StreamingResponse`](https://www.starlette.io/responses/#streamingresponse) and adds `subtype` and `boundary`.
- `media_type="application/json"` does not serialize a Python value. Pass a JSON string or bytes.

*`MultipartResponse` subclasses Starlette's native [`StreamingResponse`](https://www.starlette.io/responses/#streamingresponse).*

## Nested parts

`MultipartResponse` accepts `Part`, `MIMEPart`, and nested `MIMEMultipart` values:

```python
from multipart_response.fastapi import MIMEMultipart, Part

alternative = MIMEMultipart(
    [
        Part("Plain text", media_type="text/plain"),
        Part("<p>HTML</p>", media_type="text/html"),
    ],
    subtype="alternative",
)
```

A sequence is buffered. A sync or async iterable streams.

## htmx

The [`hx-multipart`](https://four.htmx.org/extensions/hx-multipart) extension swaps each part as it arrives.

<img src="https://raw.githubusercontent.com/scriptogre/multipart-response/main/docs/hx-multipart.png" alt="The hx-multipart extension documentation" width="720">

Use [`HX-Target`, `HX-Swap`, and `HX-Select`](https://four.htmx.org/extensions/hx-multipart#hx--headers) to control each part.

It uses [`fetch-multipart`](https://github.com/scriptogre/fetch-multipart) to parse the stream.

## Core

The dependency-free core exports `MIMEMultipart`, `MIMEPart`, and `MIMEMultipartWriter`.

- Boundaries and MIME headers are validated against RFC 2046 limits.
- Body chunks are checked for boundary collisions.
- Static, streamed, and nested multipart content is supported.

## License

BSD-3-Clause
