Spaces:
Running
Running
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 | |