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