Spaces:
Running
Running
File size: 5,337 Bytes
47b2311 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
from __future__ import annotations
import re
import typing as t
from datetime import datetime
from .._internal import _dt_as_utc
from ..http import generate_etag
from ..http import parse_date
from ..http import parse_etags
from ..http import parse_if_range_header
from ..http import unquote_etag
_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
def is_resource_modified(
http_range: str | None = None,
http_if_range: str | None = None,
http_if_modified_since: str | None = None,
http_if_none_match: str | None = None,
http_if_match: str | None = None,
etag: str | None = None,
data: bytes | None = None,
last_modified: datetime | str | None = None,
ignore_if_range: bool = True,
) -> bool:
"""Convenience method for conditional requests.
:param http_range: Range HTTP header
:param http_if_range: If-Range HTTP header
:param http_if_modified_since: If-Modified-Since HTTP header
:param http_if_none_match: If-None-Match HTTP header
:param http_if_match: If-Match HTTP header
:param etag: the etag for the response for comparison.
:param data: or alternatively the data of the response to automatically
generate an etag using :func:`generate_etag`.
:param last_modified: an optional date of the last modification.
:param ignore_if_range: If `False`, `If-Range` header will be taken into
account.
:return: `True` if the resource was modified, otherwise `False`.
.. versionadded:: 2.2
"""
if etag is None and data is not None:
etag = generate_etag(data)
elif data is not None:
raise TypeError("both data and etag given")
unmodified = False
if isinstance(last_modified, str):
last_modified = parse_date(last_modified)
# HTTP doesn't use microsecond, remove it to avoid false positive
# comparisons. Mark naive datetimes as UTC.
if last_modified is not None:
last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
if_range = None
if not ignore_if_range and http_range is not None:
# https://tools.ietf.org/html/rfc7233#section-3.2
# A server MUST ignore an If-Range header field received in a request
# that does not contain a Range header field.
if_range = parse_if_range_header(http_if_range)
if if_range is not None and if_range.date is not None:
modified_since: datetime | None = if_range.date
else:
modified_since = parse_date(http_if_modified_since)
if modified_since and last_modified and last_modified <= modified_since:
unmodified = True
if etag:
etag, _ = unquote_etag(etag)
if if_range is not None and if_range.etag is not None:
unmodified = parse_etags(if_range.etag).contains(etag)
else:
if_none_match = parse_etags(http_if_none_match)
if if_none_match:
# https://tools.ietf.org/html/rfc7232#section-3.2
# "A recipient MUST use the weak comparison function when comparing
# entity-tags for If-None-Match"
unmodified = if_none_match.contains_weak(etag)
# https://tools.ietf.org/html/rfc7232#section-3.1
# "Origin server MUST use the strong comparison function when
# comparing entity-tags for If-Match"
if_match = parse_etags(http_if_match)
if if_match:
unmodified = not if_match.is_strong(etag)
return not unmodified
_cookie_re = re.compile(
r"""
([^=;]*)
(?:\s*=\s*
(
"(?:[^\\"]|\\.)*"
|
.*?
)
)?
\s*;\s*
""",
flags=re.ASCII | re.VERBOSE,
)
_cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)")
def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes:
v = m.group(1)
if len(v) == 1:
return v
return int(v, 8).to_bytes(1, "big")
def parse_cookie(
cookie: str | None = None,
cls: type[ds.MultiDict[str, str]] | None = None,
) -> ds.MultiDict[str, str]:
"""Parse a cookie from a string.
The same key can be provided multiple times, the values are stored
in-order. The default :class:`MultiDict` will have the first value
first, and all values can be retrieved with
:meth:`MultiDict.getlist`.
:param cookie: The cookie header as a string.
:param cls: A dict-like class to store the parsed cookies in.
Defaults to :class:`MultiDict`.
.. versionchanged:: 3.0
Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
.. versionadded:: 2.2
"""
if cls is None:
cls = t.cast("type[ds.MultiDict[str, str]]", ds.MultiDict)
if not cookie:
return cls()
cookie = f"{cookie};"
out = []
for ck, cv in _cookie_re.findall(cookie):
ck = ck.strip()
cv = cv.strip()
if not ck:
continue
if len(cv) >= 2 and cv[0] == cv[-1] == '"':
# Work with bytes here, since a UTF-8 character could be multiple bytes.
cv = _cookie_unslash_re.sub(
_cookie_unslash_replace, cv[1:-1].encode()
).decode(errors="replace")
out.append((ck, cv))
return cls(out)
# circular dependencies
from .. import datastructures as ds
|