Metadata-Version: 2.4
Name: parse-multipart-form-data
Version: 0.1.0a0
Summary: A streaming parser for multipart/form-data request bodies.
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/jifengwu2k/parse-multipart-form-data
Project-URL: Bug Tracker, https://github.com/jifengwu2k/parse-multipart-form-data/issues
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=2
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: enum34; python_version < "3.4"
Requires-Dist: parse-http-header-line
Requires-Dist: put-back-iterator
Requires-Dist: typing; python_version < "3.5"
Dynamic: license-file

# parse-multipart-form-data

A small, streaming parser for HTTP `multipart/form-data` request bodies.

The parser consumes an iterable of byte chunks and yields a streaming event
sequence for each uploaded file. It does not depend on a web framework or read
from sockets itself, so callers control request-body I/O and where uploaded
content is stored.

## Installation

```bash
pip install parse-multipart-form-data
```

For local development from this checkout:

```bash
pip install -e .
```

## Usage

```python
from parse_multipart_form_data import (
    PartBegin,
    PartData,
    PartEnd,
    parse_multipart_form_data,
)

body_chunks = [
    b"--BOUNDARY\r\n",
    b'Content-Disposition: form-data; name="file"; filename="hello.txt"\r\n',
    b"\r\n",
    b"hello world\r\n",
    b"--BOUNDARY--\r\n",
]

output = None
for event in parse_multipart_form_data(
        "multipart/form-data; boundary=BOUNDARY", body_chunks):
    if isinstance(event, PartBegin):
        output = open(event.filename, "wb")
    elif isinstance(event, PartData):
        output.write(event.data)
    else:  # PartEnd
        output.close()
```

The event stream is:

- `PartBegin(filename)` at the beginning of a file part;
- `PartData(bytes_chunk)` for each file-content chunk; and
- `PartEnd(is_final)` after its delimiter has been consumed.

Only parts with a `filename` or `filename*` parameter produce events; ordinary
form fields are consumed and skipped. A valid RFC 6266 `filename*` (UTF-8 or
ISO-8859-1) takes precedence over `filename`.

## Delimiter compatibility

Only exact delimiter lines are supported:

```text
--BOUNDARY\r\n
--BOUNDARY--\r\n
```

MIME transport padding (spaces or tabs after a delimiter) is not supported.
Supporting arbitrary transport padding requires byte-by-byte input reading,
which is inefficient in Python and rare in real-world multipart form uploads.

Boundary values may be quoted or unquoted, including RFC 2046 boundary
characters such as `=`, `:`, `/`, `?`, `(`, `)`, and `,`. Preamble and
part-header lines are limited to 8192 bytes of content.

## Parser states

The parser has one `MultipartState` vocabulary. Its transitions are:

```text
SEEK_OPENING_BOUNDARY -> READ_HEADERS | DONE
READ_HEADERS          -> MAYBE_BOUNDARY
READ_PART_BODY        -> READ_PART_BODY | MAYBE_BOUNDARY
MAYBE_BOUNDARY        -> MAYBE_BOUNDARY | READ_PART_BODY | READ_HEADERS | DONE
```

### Minimal wire-format machine

For the multipart example in the module documentation, the parser's essential
wire-level transitions are (`CRLF` denotes the literal `\r\n` byte pair):

```mermaid
stateDiagram-v2
    [*] --> SEEK_OPENING_BOUNDARY
    SEEK_OPENING_BOUNDARY --> READ_HEADERS: --BOUNDARY CRLF
    SEEK_OPENING_BOUNDARY --> DONE: --BOUNDARY-- CRLF
    READ_HEADERS --> READ_HEADERS: header CRLF
    READ_HEADERS --> MAYBE_BOUNDARY: blank CRLF / empty part check
    READ_PART_BODY --> READ_PART_BODY: not *.CRLF
    READ_PART_BODY --> MAYBE_BOUNDARY: *.CRLF
    MAYBE_BOUNDARY --> MAYBE_BOUNDARY: non-boundary line ending CRLF
    MAYBE_BOUNDARY --> READ_PART_BODY: non-boundary prefix
    MAYBE_BOUNDARY --> READ_HEADERS: --BOUNDARY CRLF
    MAYBE_BOUNDARY --> DONE: --BOUNDARY-- CRLF
    DONE --> [*]
```

The single parser generator holds its state across every event. A boundary
suffix is consumed before it emits `PartEnd`, so the next event always starts
a well-defined next transition.

## Testing

```bash
python -m unittest discover -s tests
```

## License

MIT. See [LICENSE](LICENSE).
