Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

fields.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. from __future__ import annotations
  2. import email.utils
  3. import mimetypes
  4. import typing
  5. _TYPE_FIELD_VALUE = typing.Union[str, bytes]
  6. _TYPE_FIELD_VALUE_TUPLE = typing.Union[
  7. _TYPE_FIELD_VALUE,
  8. typing.Tuple[str, _TYPE_FIELD_VALUE],
  9. typing.Tuple[str, _TYPE_FIELD_VALUE, str],
  10. ]
  11. def guess_content_type(
  12. filename: str | None, default: str = "application/octet-stream"
  13. ) -> str:
  14. """
  15. Guess the "Content-Type" of a file.
  16. :param filename:
  17. The filename to guess the "Content-Type" of using :mod:`mimetypes`.
  18. :param default:
  19. If no "Content-Type" can be guessed, default to `default`.
  20. """
  21. if filename:
  22. return mimetypes.guess_type(filename)[0] or default
  23. return default
  24. def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str:
  25. """
  26. Helper function to format and quote a single header parameter using the
  27. strategy defined in RFC 2231.
  28. Particularly useful for header parameters which might contain
  29. non-ASCII values, like file names. This follows
  30. `RFC 2388 Section 4.4 <https://tools.ietf.org/html/rfc2388#section-4.4>`_.
  31. :param name:
  32. The name of the parameter, a string expected to be ASCII only.
  33. :param value:
  34. The value of the parameter, provided as ``bytes`` or `str``.
  35. :returns:
  36. An RFC-2231-formatted unicode string.
  37. .. deprecated:: 2.0.0
  38. Will be removed in urllib3 v2.1.0. This is not valid for
  39. ``multipart/form-data`` header parameters.
  40. """
  41. import warnings
  42. warnings.warn(
  43. "'format_header_param_rfc2231' is deprecated and will be "
  44. "removed in urllib3 v2.1.0. This is not valid for "
  45. "multipart/form-data header parameters.",
  46. DeprecationWarning,
  47. stacklevel=2,
  48. )
  49. if isinstance(value, bytes):
  50. value = value.decode("utf-8")
  51. if not any(ch in value for ch in '"\\\r\n'):
  52. result = f'{name}="{value}"'
  53. try:
  54. result.encode("ascii")
  55. except (UnicodeEncodeError, UnicodeDecodeError):
  56. pass
  57. else:
  58. return result
  59. value = email.utils.encode_rfc2231(value, "utf-8")
  60. value = f"{name}*={value}"
  61. return value
  62. def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str:
  63. """
  64. Format and quote a single multipart header parameter.
  65. This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching
  66. the behavior of current browser and curl versions. Values are
  67. assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are
  68. percent encoded.
  69. .. _WHATWG HTML Standard:
  70. https://html.spec.whatwg.org/multipage/
  71. form-control-infrastructure.html#multipart-form-data
  72. :param name:
  73. The name of the parameter, an ASCII-only ``str``.
  74. :param value:
  75. The value of the parameter, a ``str`` or UTF-8 encoded
  76. ``bytes``.
  77. :returns:
  78. A string ``name="value"`` with the escaped value.
  79. .. versionchanged:: 2.0.0
  80. Matches the WHATWG HTML Standard as of 2021/06/10. Control
  81. characters are no longer percent encoded.
  82. .. versionchanged:: 2.0.0
  83. Renamed from ``format_header_param_html5`` and
  84. ``format_header_param``. The old names will be removed in
  85. urllib3 v2.1.0.
  86. """
  87. if isinstance(value, bytes):
  88. value = value.decode("utf-8")
  89. # percent encode \n \r "
  90. value = value.translate({10: "%0A", 13: "%0D", 34: "%22"})
  91. return f'{name}="{value}"'
  92. def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str:
  93. """
  94. .. deprecated:: 2.0.0
  95. Renamed to :func:`format_multipart_header_param`. Will be
  96. removed in urllib3 v2.1.0.
  97. """
  98. import warnings
  99. warnings.warn(
  100. "'format_header_param_html5' has been renamed to "
  101. "'format_multipart_header_param'. The old name will be "
  102. "removed in urllib3 v2.1.0.",
  103. DeprecationWarning,
  104. stacklevel=2,
  105. )
  106. return format_multipart_header_param(name, value)
  107. def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str:
  108. """
  109. .. deprecated:: 2.0.0
  110. Renamed to :func:`format_multipart_header_param`. Will be
  111. removed in urllib3 v2.1.0.
  112. """
  113. import warnings
  114. warnings.warn(
  115. "'format_header_param' has been renamed to "
  116. "'format_multipart_header_param'. The old name will be "
  117. "removed in urllib3 v2.1.0.",
  118. DeprecationWarning,
  119. stacklevel=2,
  120. )
  121. return format_multipart_header_param(name, value)
  122. class RequestField:
  123. """
  124. A data container for request body parameters.
  125. :param name:
  126. The name of this request field. Must be unicode.
  127. :param data:
  128. The data/value body.
  129. :param filename:
  130. An optional filename of the request field. Must be unicode.
  131. :param headers:
  132. An optional dict-like object of headers to initially use for the field.
  133. .. versionchanged:: 2.0.0
  134. The ``header_formatter`` parameter is deprecated and will
  135. be removed in urllib3 v2.1.0.
  136. """
  137. def __init__(
  138. self,
  139. name: str,
  140. data: _TYPE_FIELD_VALUE,
  141. filename: str | None = None,
  142. headers: typing.Mapping[str, str] | None = None,
  143. header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None,
  144. ):
  145. self._name = name
  146. self._filename = filename
  147. self.data = data
  148. self.headers: dict[str, str | None] = {}
  149. if headers:
  150. self.headers = dict(headers)
  151. if header_formatter is not None:
  152. import warnings
  153. warnings.warn(
  154. "The 'header_formatter' parameter is deprecated and "
  155. "will be removed in urllib3 v2.1.0.",
  156. DeprecationWarning,
  157. stacklevel=2,
  158. )
  159. self.header_formatter = header_formatter
  160. else:
  161. self.header_formatter = format_multipart_header_param
  162. @classmethod
  163. def from_tuples(
  164. cls,
  165. fieldname: str,
  166. value: _TYPE_FIELD_VALUE_TUPLE,
  167. header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None,
  168. ) -> RequestField:
  169. """
  170. A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.
  171. Supports constructing :class:`~urllib3.fields.RequestField` from
  172. parameter of key/value strings AND key/filetuple. A filetuple is a
  173. (filename, data, MIME type) tuple where the MIME type is optional.
  174. For example::
  175. 'foo': 'bar',
  176. 'fakefile': ('foofile.txt', 'contents of foofile'),
  177. 'realfile': ('barfile.txt', open('realfile').read()),
  178. 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'),
  179. 'nonamefile': 'contents of nonamefile field',
  180. Field names and filenames must be unicode.
  181. """
  182. filename: str | None
  183. content_type: str | None
  184. data: _TYPE_FIELD_VALUE
  185. if isinstance(value, tuple):
  186. if len(value) == 3:
  187. filename, data, content_type = typing.cast(
  188. typing.Tuple[str, _TYPE_FIELD_VALUE, str], value
  189. )
  190. else:
  191. filename, data = typing.cast(
  192. typing.Tuple[str, _TYPE_FIELD_VALUE], value
  193. )
  194. content_type = guess_content_type(filename)
  195. else:
  196. filename = None
  197. content_type = None
  198. data = value
  199. request_param = cls(
  200. fieldname, data, filename=filename, header_formatter=header_formatter
  201. )
  202. request_param.make_multipart(content_type=content_type)
  203. return request_param
  204. def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str:
  205. """
  206. Override this method to change how each multipart header
  207. parameter is formatted. By default, this calls
  208. :func:`format_multipart_header_param`.
  209. :param name:
  210. The name of the parameter, an ASCII-only ``str``.
  211. :param value:
  212. The value of the parameter, a ``str`` or UTF-8 encoded
  213. ``bytes``.
  214. :meta public:
  215. """
  216. return self.header_formatter(name, value)
  217. def _render_parts(
  218. self,
  219. header_parts: (
  220. dict[str, _TYPE_FIELD_VALUE | None]
  221. | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]]
  222. ),
  223. ) -> str:
  224. """
  225. Helper function to format and quote a single header.
  226. Useful for single headers that are composed of multiple items. E.g.,
  227. 'Content-Disposition' fields.
  228. :param header_parts:
  229. A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format
  230. as `k1="v1"; k2="v2"; ...`.
  231. """
  232. iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]]
  233. parts = []
  234. if isinstance(header_parts, dict):
  235. iterable = header_parts.items()
  236. else:
  237. iterable = header_parts
  238. for name, value in iterable:
  239. if value is not None:
  240. parts.append(self._render_part(name, value))
  241. return "; ".join(parts)
  242. def render_headers(self) -> str:
  243. """
  244. Renders the headers for this request field.
  245. """
  246. lines = []
  247. sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"]
  248. for sort_key in sort_keys:
  249. if self.headers.get(sort_key, False):
  250. lines.append(f"{sort_key}: {self.headers[sort_key]}")
  251. for header_name, header_value in self.headers.items():
  252. if header_name not in sort_keys:
  253. if header_value:
  254. lines.append(f"{header_name}: {header_value}")
  255. lines.append("\r\n")
  256. return "\r\n".join(lines)
  257. def make_multipart(
  258. self,
  259. content_disposition: str | None = None,
  260. content_type: str | None = None,
  261. content_location: str | None = None,
  262. ) -> None:
  263. """
  264. Makes this request field into a multipart request field.
  265. This method overrides "Content-Disposition", "Content-Type" and
  266. "Content-Location" headers to the request parameter.
  267. :param content_disposition:
  268. The 'Content-Disposition' of the request body. Defaults to 'form-data'
  269. :param content_type:
  270. The 'Content-Type' of the request body.
  271. :param content_location:
  272. The 'Content-Location' of the request body.
  273. """
  274. content_disposition = (content_disposition or "form-data") + "; ".join(
  275. [
  276. "",
  277. self._render_parts(
  278. (("name", self._name), ("filename", self._filename))
  279. ),
  280. ]
  281. )
  282. self.headers["Content-Disposition"] = content_disposition
  283. self.headers["Content-Type"] = content_type
  284. self.headers["Content-Location"] = content_location