Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/sansio/http.py: 24%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import re
4import typing as t
5from datetime import datetime
7from .._internal import _dt_as_utc
8from ..http import generate_etag
9from ..http import parse_date
10from ..http import parse_etags
11from ..http import parse_if_range_header
12from ..http import unquote_etag
15def is_resource_modified(
16 http_range: str | None = None,
17 http_if_range: str | None = None,
18 http_if_modified_since: str | None = None,
19 http_if_none_match: str | None = None,
20 http_if_match: str | None = None,
21 etag: str | None = None,
22 data: bytes | None = None,
23 last_modified: datetime | str | None = None,
24 ignore_if_range: bool = True,
25) -> bool:
26 """Convenience method for conditional requests.
27 :param http_range: Range HTTP header
28 :param http_if_range: If-Range HTTP header
29 :param http_if_modified_since: If-Modified-Since HTTP header
30 :param http_if_none_match: If-None-Match HTTP header
31 :param http_if_match: If-Match HTTP header
32 :param etag: the etag for the response for comparison.
33 :param data: or alternatively the data of the response to automatically
34 generate an etag using :func:`generate_etag`.
35 :param last_modified: an optional date of the last modification.
36 :param ignore_if_range: If `False`, `If-Range` header will be taken into
37 account.
38 :return: `True` if the resource was modified, otherwise `False`.
40 .. versionadded:: 2.2
41 """
42 if etag is None and data is not None:
43 etag = f'"{generate_etag(data)}"'
44 elif data is not None:
45 raise TypeError("both data and etag given")
47 unmodified = False
48 if isinstance(last_modified, str):
49 last_modified = parse_date(last_modified)
51 # HTTP doesn't use microsecond, remove it to avoid false positive
52 # comparisons. Mark naive datetimes as UTC.
53 if last_modified is not None:
54 last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
56 if_range = None
57 if not ignore_if_range and http_range is not None:
58 # https://tools.ietf.org/html/rfc7233#section-3.2
59 # A server MUST ignore an If-Range header field received in a request
60 # that does not contain a Range header field.
61 if_range = parse_if_range_header(http_if_range)
63 if if_range is not None and if_range.date is not None:
64 modified_since: datetime | None = if_range.date
65 else:
66 modified_since = parse_date(http_if_modified_since)
68 if modified_since and last_modified and last_modified <= modified_since:
69 unmodified = True
71 if etag:
72 etag, _ = unquote_etag(etag)
74 if if_range is not None and if_range.etag is not None:
75 unmodified = if_range.etag == etag
76 else:
77 if_none_match = parse_etags(http_if_none_match)
78 if if_none_match:
79 # https://tools.ietf.org/html/rfc7232#section-3.2
80 # "A recipient MUST use the weak comparison function when comparing
81 # entity-tags for If-None-Match"
82 unmodified = if_none_match.contains_weak(etag)
84 # https://tools.ietf.org/html/rfc7232#section-3.1
85 # "Origin server MUST use the strong comparison function when
86 # comparing entity-tags for If-Match"
87 if_match = parse_etags(http_if_match)
88 if if_match:
89 unmodified = not if_match.is_strong(etag)
91 return not unmodified
94_cookie_re = re.compile(
95 r"""
96 [ \t]* # ignore leading space
97 ([^ \t=";]+) # key
98 (?:[ \t]*=[ \t]* # optional =value, ignoring invalid space
99 (
100 "(?:[^\\"]|\\.)*" # quoted value with backslash escapes
101 |
102 [^ \t";]* # token value
103 )
104 )?
105 [ \t]* # ignore trailing space
106 (?:;|\Z) # only if followed by semicolon or end
107 """,
108 flags=re.ASCII | re.VERBOSE,
109)
110_cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)")
113def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes:
114 v = m.group(1)
116 if len(v) == 1:
117 return v
119 return int(v, 8).to_bytes(1, "big")
122def parse_cookie(
123 cookie: str | None = None,
124 cls: type[ds.MultiDict[str, str]] | None = None,
125) -> ds.MultiDict[str, str]:
126 """Parse a cookie from a string.
128 The same key can be provided multiple times, the values are stored
129 in-order. The default :class:`MultiDict` will have the first value
130 first, and all values can be retrieved with
131 :meth:`MultiDict.getlist`.
133 :param cookie: The cookie header as a string.
134 :param cls: A dict-like class to store the parsed cookies in.
135 Defaults to :class:`MultiDict`.
137 .. versionchanged:: 3.0
138 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
140 .. versionadded:: 2.2
141 """
142 if cls is None:
143 cls = t.cast("type[ds.MultiDict[str, str]]", ds.MultiDict)
145 if not cookie:
146 return cls()
148 out = []
149 pos = 0
151 while True:
152 if (m := _cookie_re.match(cookie, pos)) is None:
153 # Skip invalid chars until the next semicolon.
154 if (pos := cookie.find(";", pos) + 1) == 0:
155 break
157 continue
159 ck, cv = m.groups()
160 pos = m.end()
162 if cv is None:
163 cv = ""
165 if cv.startswith('"') and cv.endswith('"'):
166 # Work with bytes here, since a UTF-8 character could be multiple bytes.
167 cv = _cookie_unslash_re.sub(
168 _cookie_unslash_replace, cv[1:-1].encode()
169 ).decode(errors="replace")
171 out.append((ck, cv))
173 return cls(out)
176# circular dependencies
177from .. import datastructures as ds # noqa: E402