defnull / multipart

A fast multipart/form-data parser for python
https://multipart.readthedocs.io/
MIT License
135 stars 33 forks source link

Add functionality to generate valid `multipart/form-data` #71

Open defnull opened 3 days ago

defnull commented 3 days ago

Provide a MultipartBuilder that can be used to produce valid multipart/form-data and offers an API that avoids user errors as much as possible. This can be used in tests or HTTP clients. Having both parsers and generators in one library seems to be a good fit.

API Idea

R = TypeVar("R")
_identity = lambda x: x

class MultipartBuilder(Generic[R]):
    def __init__(self, boundary: str, write_func: Callable[[Union[bytes,bytearray]], R] = _identity):
        " Create a new builder. "

    def add_part(self, name: str, filename: Optional[str] = None, content_type: Optional[str] = None) -> R:
        "Start a new text field or file upload."

    def write(self, chunk: Union[str, bytes, bytearray]) -> R:
        "Write a chunk of data to the current field."

    def close(self) -> R:
        "Write the final delimiter"

The write_func is responsible for writing a single bytearray to the target stream, or return a value that represents the intent to do so. The builder functions return whatever the write function returns. This allows this builder to be used in both blocking and non-blocking environments. If you pass in an async function, then the return value R will be a coroutine you can await.

# Blocking
def blocking(target: io.BufferedWriter):
    with MultipartBuilder("--foo", target.write) as builder:
        builder.add_part("name", "filename.txt")
        builder.write("content")

# Async
async def non_blocking(target: asyncio.StreamWriter):
    async with MultipartBuilder("--foo", target.write) as builder:
        await builder.add_part("name", "filename.txt")
        await builder.write("content")

# SansIO
def sans_io() -> Iterator[bytes]:
    builder = MultipartBuilder("--foo", lambda x: x)
    yield builder.add_part("name", "filename.txt")
    yield builder.write("content")
    yield builder.close()

Not sure if this is a good idea, though. It might be a little bit to clever and having dedicated APIs for AsyncMultipartBuilder and SansIOMultipartBuilder with proper method signatures may be more intuitive.