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