jpayne@69: from __future__ import annotations jpayne@69: jpayne@69: import binascii jpayne@69: import codecs jpayne@69: import os jpayne@69: import typing jpayne@69: from io import BytesIO jpayne@69: jpayne@69: from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField jpayne@69: jpayne@69: writer = codecs.lookup("utf-8")[3] jpayne@69: jpayne@69: _TYPE_FIELDS_SEQUENCE = typing.Sequence[ jpayne@69: typing.Union[typing.Tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] jpayne@69: ] jpayne@69: _TYPE_FIELDS = typing.Union[ jpayne@69: _TYPE_FIELDS_SEQUENCE, jpayne@69: typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], jpayne@69: ] jpayne@69: jpayne@69: jpayne@69: def choose_boundary() -> str: jpayne@69: """ jpayne@69: Our embarrassingly-simple replacement for mimetools.choose_boundary. jpayne@69: """ jpayne@69: return binascii.hexlify(os.urandom(16)).decode() jpayne@69: jpayne@69: jpayne@69: def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: jpayne@69: """ jpayne@69: Iterate over fields. jpayne@69: jpayne@69: Supports list of (k, v) tuples and dicts, and lists of jpayne@69: :class:`~urllib3.fields.RequestField`. jpayne@69: jpayne@69: """ jpayne@69: iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] jpayne@69: jpayne@69: if isinstance(fields, typing.Mapping): jpayne@69: iterable = fields.items() jpayne@69: else: jpayne@69: iterable = fields jpayne@69: jpayne@69: for field in iterable: jpayne@69: if isinstance(field, RequestField): jpayne@69: yield field jpayne@69: else: jpayne@69: yield RequestField.from_tuples(*field) jpayne@69: jpayne@69: jpayne@69: def encode_multipart_formdata( jpayne@69: fields: _TYPE_FIELDS, boundary: str | None = None jpayne@69: ) -> tuple[bytes, str]: jpayne@69: """ jpayne@69: Encode a dictionary of ``fields`` using the multipart/form-data MIME format. jpayne@69: jpayne@69: :param fields: jpayne@69: Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). jpayne@69: Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. jpayne@69: jpayne@69: :param boundary: jpayne@69: If not specified, then a random boundary will be generated using jpayne@69: :func:`urllib3.filepost.choose_boundary`. jpayne@69: """ jpayne@69: body = BytesIO() jpayne@69: if boundary is None: jpayne@69: boundary = choose_boundary() jpayne@69: jpayne@69: for field in iter_field_objects(fields): jpayne@69: body.write(f"--{boundary}\r\n".encode("latin-1")) jpayne@69: jpayne@69: writer(body).write(field.render_headers()) jpayne@69: data = field.data jpayne@69: jpayne@69: if isinstance(data, int): jpayne@69: data = str(data) # Backwards compatibility jpayne@69: jpayne@69: if isinstance(data, str): jpayne@69: writer(body).write(data) jpayne@69: else: jpayne@69: body.write(data) jpayne@69: jpayne@69: body.write(b"\r\n") jpayne@69: jpayne@69: body.write(f"--{boundary}--\r\n".encode("latin-1")) jpayne@69: jpayne@69: content_type = f"multipart/form-data; boundary={boundary}" jpayne@69: jpayne@69: return body.getvalue(), content_type