http.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import re
  2. import typing as t
  3. from datetime import datetime
  4. from .._internal import _cookie_parse_impl
  5. from .._internal import _dt_as_utc
  6. from .._internal import _to_str
  7. from ..http import generate_etag
  8. from ..http import parse_date
  9. from ..http import parse_etags
  10. from ..http import parse_if_range_header
  11. from ..http import unquote_etag
  12. _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
  13. def is_resource_modified(
  14. http_range: t.Optional[str] = None,
  15. http_if_range: t.Optional[str] = None,
  16. http_if_modified_since: t.Optional[str] = None,
  17. http_if_none_match: t.Optional[str] = None,
  18. http_if_match: t.Optional[str] = None,
  19. etag: t.Optional[str] = None,
  20. data: t.Optional[bytes] = None,
  21. last_modified: t.Optional[t.Union[datetime, str]] = None,
  22. ignore_if_range: bool = True,
  23. ) -> bool:
  24. """Convenience method for conditional requests.
  25. :param http_range: Range HTTP header
  26. :param http_if_range: If-Range HTTP header
  27. :param http_if_modified_since: If-Modified-Since HTTP header
  28. :param http_if_none_match: If-None-Match HTTP header
  29. :param http_if_match: If-Match HTTP header
  30. :param etag: the etag for the response for comparison.
  31. :param data: or alternatively the data of the response to automatically
  32. generate an etag using :func:`generate_etag`.
  33. :param last_modified: an optional date of the last modification.
  34. :param ignore_if_range: If `False`, `If-Range` header will be taken into
  35. account.
  36. :return: `True` if the resource was modified, otherwise `False`.
  37. .. versionadded:: 2.2
  38. """
  39. if etag is None and data is not None:
  40. etag = generate_etag(data)
  41. elif data is not None:
  42. raise TypeError("both data and etag given")
  43. unmodified = False
  44. if isinstance(last_modified, str):
  45. last_modified = parse_date(last_modified)
  46. # HTTP doesn't use microsecond, remove it to avoid false positive
  47. # comparisons. Mark naive datetimes as UTC.
  48. if last_modified is not None:
  49. last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
  50. if_range = None
  51. if not ignore_if_range and http_range is not None:
  52. # https://tools.ietf.org/html/rfc7233#section-3.2
  53. # A server MUST ignore an If-Range header field received in a request
  54. # that does not contain a Range header field.
  55. if_range = parse_if_range_header(http_if_range)
  56. if if_range is not None and if_range.date is not None:
  57. modified_since: t.Optional[datetime] = if_range.date
  58. else:
  59. modified_since = parse_date(http_if_modified_since)
  60. if modified_since and last_modified and last_modified <= modified_since:
  61. unmodified = True
  62. if etag:
  63. etag, _ = unquote_etag(etag)
  64. etag = t.cast(str, etag)
  65. if if_range is not None and if_range.etag is not None:
  66. unmodified = parse_etags(if_range.etag).contains(etag)
  67. else:
  68. if_none_match = parse_etags(http_if_none_match)
  69. if if_none_match:
  70. # https://tools.ietf.org/html/rfc7232#section-3.2
  71. # "A recipient MUST use the weak comparison function when comparing
  72. # entity-tags for If-None-Match"
  73. unmodified = if_none_match.contains_weak(etag)
  74. # https://tools.ietf.org/html/rfc7232#section-3.1
  75. # "Origin server MUST use the strong comparison function when
  76. # comparing entity-tags for If-Match"
  77. if_match = parse_etags(http_if_match)
  78. if if_match:
  79. unmodified = not if_match.is_strong(etag)
  80. return not unmodified
  81. def parse_cookie(
  82. cookie: t.Union[bytes, str, None] = "",
  83. charset: str = "utf-8",
  84. errors: str = "replace",
  85. cls: t.Optional[t.Type["ds.MultiDict"]] = None,
  86. ) -> "ds.MultiDict[str, str]":
  87. """Parse a cookie from a string.
  88. The same key can be provided multiple times, the values are stored
  89. in-order. The default :class:`MultiDict` will have the first value
  90. first, and all values can be retrieved with
  91. :meth:`MultiDict.getlist`.
  92. :param cookie: The cookie header as a string.
  93. :param charset: The charset for the cookie values.
  94. :param errors: The error behavior for the charset decoding.
  95. :param cls: A dict-like class to store the parsed cookies in.
  96. Defaults to :class:`MultiDict`.
  97. .. versionadded:: 2.2
  98. """
  99. # PEP 3333 sends headers through the environ as latin1 decoded
  100. # strings. Encode strings back to bytes for parsing.
  101. if isinstance(cookie, str):
  102. cookie = cookie.encode("latin1", "replace")
  103. if cls is None:
  104. cls = ds.MultiDict
  105. def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]:
  106. for key, val in _cookie_parse_impl(cookie): # type: ignore
  107. key_str = _to_str(key, charset, errors, allow_none_charset=True)
  108. if not key_str:
  109. continue
  110. val_str = _to_str(val, charset, errors, allow_none_charset=True)
  111. yield key_str, val_str
  112. return cls(_parse_pairs())
  113. # circular dependencies
  114. from .. import datastructures as ds