jpayne@7
|
1 from __future__ import annotations
|
jpayne@7
|
2
|
jpayne@7
|
3 import binascii
|
jpayne@7
|
4 import codecs
|
jpayne@7
|
5 import os
|
jpayne@7
|
6 import typing
|
jpayne@7
|
7 from io import BytesIO
|
jpayne@7
|
8
|
jpayne@7
|
9 from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField
|
jpayne@7
|
10
|
jpayne@7
|
11 writer = codecs.lookup("utf-8")[3]
|
jpayne@7
|
12
|
jpayne@7
|
13 _TYPE_FIELDS_SEQUENCE = typing.Sequence[
|
jpayne@7
|
14 typing.Union[typing.Tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField]
|
jpayne@7
|
15 ]
|
jpayne@7
|
16 _TYPE_FIELDS = typing.Union[
|
jpayne@7
|
17 _TYPE_FIELDS_SEQUENCE,
|
jpayne@7
|
18 typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE],
|
jpayne@7
|
19 ]
|
jpayne@7
|
20
|
jpayne@7
|
21
|
jpayne@7
|
22 def choose_boundary() -> str:
|
jpayne@7
|
23 """
|
jpayne@7
|
24 Our embarrassingly-simple replacement for mimetools.choose_boundary.
|
jpayne@7
|
25 """
|
jpayne@7
|
26 return binascii.hexlify(os.urandom(16)).decode()
|
jpayne@7
|
27
|
jpayne@7
|
28
|
jpayne@7
|
29 def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]:
|
jpayne@7
|
30 """
|
jpayne@7
|
31 Iterate over fields.
|
jpayne@7
|
32
|
jpayne@7
|
33 Supports list of (k, v) tuples and dicts, and lists of
|
jpayne@7
|
34 :class:`~urllib3.fields.RequestField`.
|
jpayne@7
|
35
|
jpayne@7
|
36 """
|
jpayne@7
|
37 iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]]
|
jpayne@7
|
38
|
jpayne@7
|
39 if isinstance(fields, typing.Mapping):
|
jpayne@7
|
40 iterable = fields.items()
|
jpayne@7
|
41 else:
|
jpayne@7
|
42 iterable = fields
|
jpayne@7
|
43
|
jpayne@7
|
44 for field in iterable:
|
jpayne@7
|
45 if isinstance(field, RequestField):
|
jpayne@7
|
46 yield field
|
jpayne@7
|
47 else:
|
jpayne@7
|
48 yield RequestField.from_tuples(*field)
|
jpayne@7
|
49
|
jpayne@7
|
50
|
jpayne@7
|
51 def encode_multipart_formdata(
|
jpayne@7
|
52 fields: _TYPE_FIELDS, boundary: str | None = None
|
jpayne@7
|
53 ) -> tuple[bytes, str]:
|
jpayne@7
|
54 """
|
jpayne@7
|
55 Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
|
jpayne@7
|
56
|
jpayne@7
|
57 :param fields:
|
jpayne@7
|
58 Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).
|
jpayne@7
|
59 Values are processed by :func:`urllib3.fields.RequestField.from_tuples`.
|
jpayne@7
|
60
|
jpayne@7
|
61 :param boundary:
|
jpayne@7
|
62 If not specified, then a random boundary will be generated using
|
jpayne@7
|
63 :func:`urllib3.filepost.choose_boundary`.
|
jpayne@7
|
64 """
|
jpayne@7
|
65 body = BytesIO()
|
jpayne@7
|
66 if boundary is None:
|
jpayne@7
|
67 boundary = choose_boundary()
|
jpayne@7
|
68
|
jpayne@7
|
69 for field in iter_field_objects(fields):
|
jpayne@7
|
70 body.write(f"--{boundary}\r\n".encode("latin-1"))
|
jpayne@7
|
71
|
jpayne@7
|
72 writer(body).write(field.render_headers())
|
jpayne@7
|
73 data = field.data
|
jpayne@7
|
74
|
jpayne@7
|
75 if isinstance(data, int):
|
jpayne@7
|
76 data = str(data) # Backwards compatibility
|
jpayne@7
|
77
|
jpayne@7
|
78 if isinstance(data, str):
|
jpayne@7
|
79 writer(body).write(data)
|
jpayne@7
|
80 else:
|
jpayne@7
|
81 body.write(data)
|
jpayne@7
|
82
|
jpayne@7
|
83 body.write(b"\r\n")
|
jpayne@7
|
84
|
jpayne@7
|
85 body.write(f"--{boundary}--\r\n".encode("latin-1"))
|
jpayne@7
|
86
|
jpayne@7
|
87 content_type = f"multipart/form-data; boundary={boundary}"
|
jpayne@7
|
88
|
jpayne@7
|
89 return body.getvalue(), content_type
|