Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/http.py: 59%
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 collections.abc as cabc
4import datetime as dt
5import email.utils
6import hashlib
7import re
8import typing as t
9import warnings
10import wsgiref.types as wt
11from base64 import b64encode
12from enum import Enum
13from time import mktime
14from time import struct_time
15from urllib.parse import quote
16from urllib.parse import unquote
18from ._internal import _dt_as_utc
19from ._internal import _plain_int
21_token_chars = frozenset(
22 "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
23)
24_HTTP_STATUS_CODES = {
25 100: "Continue",
26 101: "Switching Protocols",
27 102: "Processing",
28 103: "Early Hints", # see RFC 8297
29 200: "OK",
30 201: "Created",
31 202: "Accepted",
32 203: "Non Authoritative Information",
33 204: "No Content",
34 205: "Reset Content",
35 206: "Partial Content",
36 207: "Multi Status",
37 208: "Already Reported", # see RFC 5842
38 226: "IM Used", # see RFC 3229
39 300: "Multiple Choices",
40 301: "Moved Permanently",
41 302: "Found",
42 303: "See Other",
43 304: "Not Modified",
44 305: "Use Proxy",
45 306: "Switch Proxy", # unused
46 307: "Temporary Redirect",
47 308: "Permanent Redirect",
48 400: "Bad Request",
49 401: "Unauthorized",
50 402: "Payment Required", # unused
51 403: "Forbidden",
52 404: "Not Found",
53 405: "Method Not Allowed",
54 406: "Not Acceptable",
55 407: "Proxy Authentication Required",
56 408: "Request Timeout",
57 409: "Conflict",
58 410: "Gone",
59 411: "Length Required",
60 412: "Precondition Failed",
61 413: "Request Entity Too Large",
62 414: "Request URI Too Long",
63 415: "Unsupported Media Type",
64 416: "Requested Range Not Satisfiable",
65 417: "Expectation Failed",
66 418: "I'm a teapot", # see RFC 2324
67 421: "Misdirected Request", # see RFC 7540
68 422: "Unprocessable Entity",
69 423: "Locked",
70 424: "Failed Dependency",
71 425: "Too Early", # see RFC 8470
72 426: "Upgrade Required",
73 428: "Precondition Required", # see RFC 6585
74 429: "Too Many Requests",
75 431: "Request Header Fields Too Large",
76 449: "Retry With", # proprietary MS extension
77 451: "Unavailable For Legal Reasons",
78 500: "Internal Server Error",
79 501: "Not Implemented",
80 502: "Bad Gateway",
81 503: "Service Unavailable",
82 504: "Gateway Timeout",
83 505: "HTTP Version Not Supported",
84 506: "Variant Also Negotiates", # see RFC 2295
85 507: "Insufficient Storage",
86 508: "Loop Detected", # see RFC 5842
87 510: "Not Extended",
88 511: "Network Authentication Failed",
89}
92class COEP(Enum):
93 """``Cross-Origin-Embedder-Policy`` header values. Used by
94 :attr:`.Response.cross_origin_embedder_policy`.
96 .. versionchanged:: 3.2
97 Added the ``credentialless`` member.
99 .. versionadded:: 2.0
100 """
102 UNSAFE_NONE = "unsafe-none"
103 REQUIRE_CORP = "require-corp"
104 CREDENTIALLESS = "credentialless"
107class COOP(Enum):
108 """``Cross-Origin-Opener-Policy`` header values. Used by
109 :attr:`.Response.cross_origin_opener_policy`.
111 .. versionchanged:: 3.2
112 Added the ``noopener-allow-popups`` member.
114 .. versionadded:: 2.0
115 """
117 UNSAFE_NONE = "unsafe-none"
118 SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"
119 SAME_ORIGIN = "same-origin"
120 NOOPENER_ALLOW_POPUPS = "noopener-allow-popups"
123class CORP(Enum):
124 """``Cross-Origin-Resource-Policy`` header values. Used by
125 :attr:`.Response.cross_origin_resource_policy`.
127 .. versionadded:: 3.2
128 """
130 SAME_SITE = "same-site"
131 SAME_ORIGIN = "same-origin"
132 CROSS_ORIGIN = "cross-origin"
135class SecFetchSite(Enum):
136 """``Sec-Fetch-Site`` header values. Used by :attr:`.Request.sec_fetch_site`.
138 .. versionadded:: 3.2
139 """
141 CROSS_SITE = "cross-site"
142 SAME_ORIGIN = "same-origin"
143 SAME_SITE = "same-site"
144 NONE = "none"
147class SecFetchMode(Enum):
148 """``Sec-Fetch-Mode`` header values. Used by :attr:`.Request.sec_fetch_mode`.
150 .. versionadded:: 3.2
151 """
153 CORS = "cors"
154 NAVIGATE = "navigate"
155 NO_CORS = "no-cors"
156 SAME_ORIGIN = "same-origin"
157 WEBSOCKET = "websocket"
160class SecFetchDest(Enum):
161 """``Sec-Fetch-Dest`` header values. Used by :attr:`.Request.sec_fetch_dest`.
163 .. versionadded:: 3.2
164 """
166 AUDIO = "audio"
167 AUDIOWORKLET = "audioworklet"
168 DOCUMENT = "document"
169 EMBED = "embed"
170 EMPTY = "empty"
171 FENCEDFRAME = "fencedframe"
172 FONT = "font"
173 FRAME = "frame"
174 IFRAME = "iframe"
175 IMAGE = "image"
176 JSON = "json"
177 MANIFEST = "manifest"
178 OBJECT = "object"
179 PAINTWORKLET = "paintworklet"
180 REPORT = "report"
181 SCRIPT = "script"
182 SERVICEWORKER = "serviceworker"
183 SHAREDWORKER = "sharedworker"
184 STYLE = "style"
185 TRACK = "track"
186 VIDEO = "video"
187 WEBIDENTITY = "webidentity"
188 WORKER = "worker"
189 XSLT = "xslt"
192def quote_header_value(value: t.Any, allow_token: bool = True) -> str:
193 """Add double quotes around a header value. If the header contains only ASCII token
194 characters, it will be returned unchanged. If the header contains ``"`` or ``\\``
195 characters, they will be escaped with an additional ``\\`` character.
197 This is the reverse of :func:`unquote_header_value`.
199 :param value: The value to quote. Will be converted to a string.
200 :param allow_token: Disable to quote the value even if it only has token characters.
202 .. versionchanged:: 3.0
203 Passing bytes is not supported.
205 .. versionchanged:: 3.0
206 The ``extra_chars`` parameter is removed.
208 .. versionchanged:: 2.3
209 The value is quoted if it is the empty string.
211 .. versionadded:: 0.5
212 """
213 value_str = str(value)
215 if not value_str:
216 return '""'
218 if allow_token:
219 token_chars = _token_chars
221 if token_chars.issuperset(value_str):
222 return value_str
224 value_str = value_str.replace("\\", "\\\\").replace('"', '\\"')
225 return f'"{value_str}"'
228_unslash_re = re.compile(r"\\(.)", re.A)
231def unquote_header_value(value: str) -> str:
232 """Remove double quotes and backslash escapes from a header value.
234 This is the reverse of :func:`quote_header_value`.
236 :param value: The header value to unquote.
238 .. versionchanged:: 3.2
239 Removes escape preceding any character.
241 .. versionchanged:: 3.0
242 The ``is_filename`` parameter is removed.
243 """
244 if len(value) >= 2 and value.startswith('"') and value.endswith('"'):
245 return _unslash_re.sub(r"\g<1>", value[1:-1])
247 return value
250def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:
251 """Produce a header value and ``key=value`` parameters separated by semicolons
252 ``;``. For example, the ``Content-Type`` header.
254 .. code-block:: python
256 dump_options_header("text/html", {"charset": "UTF-8"})
257 'text/html; charset=UTF-8'
259 This is the reverse of :func:`parse_options_header`.
261 If a value contains non-token characters, it will be quoted.
263 If a value is ``None``, the parameter is skipped.
265 In some keys for some headers, a UTF-8 value can be encoded using a special
266 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
267 not produce that format automatically, but if a given key ends with an asterisk
268 ``*``, the value is assumed to have that form and will not be quoted further.
270 :param header: The primary header value.
271 :param options: Parameters to encode as ``key=value`` pairs.
273 .. versionchanged:: 2.3
274 Keys with ``None`` values are skipped rather than treated as a bare key.
276 .. versionchanged:: 2.2.3
277 If a key ends with ``*``, its value will not be quoted.
278 """
279 segments = []
281 if header is not None:
282 segments.append(header)
284 for key, value in options.items():
285 if value is None:
286 continue
288 if key.endswith("*"):
289 segments.append(f"{key}={value}")
290 else:
291 segments.append(f"{key}={quote_header_value(value)}")
293 return "; ".join(segments)
296def dump_header(iterable: cabc.Mapping[str, t.Any] | cabc.Iterable[t.Any]) -> str:
297 """Produce a header value from a list of items or ``key=value`` pairs, separated by
298 commas ``,``.
300 This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and
301 :func:`parse_set_header`.
303 If a value contains non-token characters, it will be quoted.
305 If a value is ``None``, the key is output alone.
307 In some keys for some headers, a UTF-8 value can be encoded using a special
308 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
309 not produce that format automatically, but if a given key ends with an asterisk
310 ``*``, the value is assumed to have that form and will not be quoted further.
312 .. code-block:: python
314 dump_header(["foo", "bar baz"])
315 'foo, "bar baz"'
317 dump_header({"foo": "bar baz"})
318 'foo="bar baz"'
320 :param iterable: The items to create a header from.
322 .. versionchanged:: 3.0
323 The ``allow_token`` parameter is removed.
325 .. versionchanged:: 2.2.3
326 If a key ends with ``*``, its value will not be quoted.
327 """
328 if isinstance(iterable, cabc.Mapping):
329 items = []
331 for key, value in iterable.items():
332 if value is None:
333 items.append(key)
334 elif key.endswith("*"):
335 items.append(f"{key}={value}")
336 else:
337 items.append(f"{key}={quote_header_value(value)}")
338 else:
339 items = [quote_header_value(x) for x in iterable]
341 return ", ".join(items)
344def _dump_csp_header(header: ds.ContentSecurityPolicy) -> str:
345 """Dump a Content Security Policy header.
347 These are structured into policies such as "default-src 'self';
348 script-src 'self'".
350 .. deprecated:: 3.2
351 Will be removed in Werkzeug 4.0. Use the
352 ``ContentSecurityPolicy.to_header`` method instead.
354 .. versionadded:: 1.0.0
355 Support for Content Security Policy headers was added.
357 """
358 return header.to_header()
361def parse_list_header(value: str) -> list[str]:
362 """Parse a header value that consists of a list of comma separated items according
363 to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.
365 Surrounding quotes are removed from items, but internal quotes are left for
366 future parsing. Empty values are discarded.
368 .. code-block:: python
370 parse_list_header('token, "quoted value"')
371 ['token', 'quoted value']
373 This is the reverse of :func:`dump_header`.
375 :param value: The header value to parse.
377 .. versionchanged:: 3.2
378 Quotes and escapes are kept if only part of an item is quoted. Empty
379 values are omitted. An empty list is returned if the value contains an
380 unclosed quoted string.
381 """
382 items = []
383 item = ""
384 escape = False
385 quote = False
387 for char in value:
388 if escape:
389 escape = False
390 item += char
391 continue
393 if quote:
394 if char == "\\":
395 escape = True
396 elif char == '"':
397 quote = False
399 item += char
400 continue
402 if char == ",":
403 items.append(item)
404 item = ""
405 continue
407 if char == '"':
408 quote = True
410 item += char
412 if quote:
413 # invalid, unclosed quoted string
414 return []
416 items.append(item)
417 return [
418 unquote_header_value(item)
419 for item in (item.strip(" \t") for item in items)
420 if item
421 ]
424def parse_dict_header(value: str) -> dict[str, str | None]:
425 """Parse a list header using :func:`parse_list_header`, then parse each item as a
426 ``key=value`` pair.
428 .. code-block:: python
430 parse_dict_header('a=b, c="d, e", f')
431 {"a": "b", "c": "d, e", "f": None}
433 This is the reverse of :func:`dump_header`.
435 If a key does not have a value, it is ``None``.
437 This handles charsets for values as described in
438 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,
439 and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.
441 :param value: The header value to parse.
443 .. versionchanged:: 3.2
444 An empty dict is returned if the value contains an unclosed quoted
445 string.
447 .. versionchanged:: 3.0
448 Passing bytes is not supported.
450 .. versionchanged:: 3.0
451 The ``cls`` argument is removed.
453 .. versionchanged:: 2.3
454 Added support for ``key*=charset''value`` encoded items.
456 .. versionchanged:: 0.9
457 The ``cls`` argument was added.
458 """
459 result: dict[str, str | None] = {}
461 for item in parse_list_header(value):
462 key, has_value, value = item.partition("=")
463 key = key.strip(" \t")
465 if not key:
466 # =value is not valid
467 continue
469 if not has_value:
470 result[key] = None
471 continue
473 value = value.strip(" \t")
474 encoding: str | None = None
476 if key.endswith("*"):
477 # key*=charset''value becomes key=value, where value is percent encoded
478 # adapted from parse_options_header, without the continuation handling
479 key = key[:-1]
481 if (m_charset := _charset_value_re.match(value)) is not None:
482 # If there is a charset marker in the value, split it off.
483 encoding, value = m_charset.groups()
484 encoding = encoding.lower()
486 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
487 # This list will not be extended further. An invalid encoding will leave the
488 # value quoted.
489 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
490 # invalid bytes are replaced during unquoting
491 value = unquote(value, encoding=encoding)
493 result[key] = unquote_header_value(value)
495 return result
498# https://httpwg.org/specs/rfc9110.html#parameter
499_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII)
500# https://www.rfc-editor.org/rfc/rfc2231#section-3
501_parameter_mark_re = re.compile(r"(?:\*(\d+))?(\*)?$", flags=re.ASCII)
502_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII)
503# https://www.rfc-editor.org/rfc/rfc2231#section-4
504_charset_value_re = re.compile(
505 r"""
506 ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty
507 [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty
508 ([\w!#$%&'*+\-.^`|~]+)$ # one or more token chars with percent encoding
509 """,
510 re.ASCII | re.VERBOSE,
511)
512_parameter_delimiter_re = re.compile(r";[ \t]*")
515def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
516 """Parse a header that consists of a value with ``key=value`` parameters separated
517 by semicolons ``;``. For example, the ``Content-Type`` header.
519 .. code-block:: python
521 parse_options_header("text/html; charset=UTF-8")
522 ('text/html', {'charset': 'UTF-8'})
524 parse_options_header("")
525 ("", {})
527 This is the reverse of :func:`dump_options_header`.
529 This parses valid parameter parts as described in
530 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are
531 skipped.
533 This handles continuations and charsets as described in
534 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as
535 strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,
536 otherwise the value remains quoted.
538 Clients may not be consistent in how they handle a quote character within a quoted
539 value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__
540 replaces it with ``%22`` in multipart form data.
541 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash
542 escapes in HTTP headers. Both are decoded to the ``"`` character.
544 Clients may not be consistent in how they handle non-ASCII characters. HTML
545 documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with
546 HTML character references, which can be decoded using :func:`html.unescape`.
548 :param value: The header value to parse.
549 :return: ``(value, options)``, where ``options`` is a dict
551 .. versionchanged:: 2.3
552 Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted
553 values, are discarded instead of treating as ``None``.
555 .. versionchanged:: 2.3
556 Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.
558 .. versionchanged:: 2.3
559 Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.
561 .. versionchanged:: 2.2
562 Option names are always converted to lowercase.
564 .. versionchanged:: 2.2
565 The ``multiple`` parameter was removed.
567 .. versionchanged:: 0.15
568 :rfc:`2231` parameter continuations are handled.
570 .. versionadded:: 0.5
571 """
572 if value is None:
573 return "", {}
575 value, _, rest = value.partition(";")
576 value = value.strip(" \t")
577 rest = rest.strip(" \t")
579 if not value or not rest:
580 # empty (invalid) value, or value without options
581 return value, {}
583 # Collect all valid key=value parts without processing the value.
584 parts: list[tuple[str, str]] = []
585 scan_pos = 0
586 length = len(rest)
588 while scan_pos < length:
589 if (m_key := _parameter_key_re.match(rest, scan_pos)) is not None:
590 pk = m_key.group(1).lower()
591 scan_pos = m_key.end()
593 # Value may be a token.
594 if (m_value := _parameter_token_value_re.match(rest, scan_pos)) is not None:
595 parts.append((pk, m_value.group()))
597 # Value may be a quoted string, find the closing quote.
598 elif rest.startswith('"', scan_pos):
599 quote_pos = scan_pos + 1
601 while quote_pos < length:
602 if rest.startswith(("\\\\", '\\"'), quote_pos):
603 # Consume escaped slashes and quotes.
604 quote_pos += 2
605 elif rest.startswith('"', quote_pos):
606 # Stop at an unescaped quote.
607 quote_pos += 1
608 parts.append((pk, rest[scan_pos:quote_pos]))
609 scan_pos = quote_pos
610 break
611 else:
612 # Consume any other character.
613 quote_pos += 1
614 else:
615 # Scanned all without finding closing quote.
616 break
618 # Continue to the next delimited part, skipping spaces.
619 if m_delim := _parameter_delimiter_re.search(rest, scan_pos):
620 scan_pos = m_delim.end()
621 else:
622 break
624 options: dict[str, str] = {}
625 continuations: dict[str, list[str]] = {}
626 continuation_encodings: dict[str, str] = {}
628 # For each collected part, process optional charset and continuation,
629 # unquote quoted values.
630 for pk, pv in parts:
631 encoding: str | None = None
632 has_continuation = has_charset = False
634 # Check for and remove markers at end of key.
635 if (
636 m_mark := _parameter_mark_re.search(pk)
637 ) is not None and m_mark.end() - m_mark.start() > 0:
638 has_continuation = m_mark.group(1) is not None
639 has_charset = m_mark.group(2) is not None
640 pk = pk[: m_mark.start()]
642 # key*=charset''value becomes key=value, where value is percent encoded
643 if has_charset:
644 if (m_charset := _charset_value_re.match(pv)) is not None:
645 # If there is a valid charset marker in the value, split it off.
646 encoding, pv = m_charset.groups()
647 # This might be the empty string, handled next.
648 encoding = encoding.lower()
650 # Use the prior continuation encoding if not set on this part.
651 if not encoding and has_continuation:
652 encoding = continuation_encodings.get(pk)
654 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
655 # This list will not be extended further. An invalid encoding will leave the
656 # value quoted.
657 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
658 # invalid bytes are replaced during unquoting
659 pv = unquote(pv, encoding=encoding)
660 else:
661 # Remove quotes, replace header slash escapes, replace multipart
662 # percent-encoded quotes.
663 pv = unquote_header_value(pv).replace("%22", '"')
665 # key*0=a; key*1=b becomes key=ab
666 if has_continuation:
667 # Remove prior normal option.
668 if pk in options:
669 del options[pk]
671 # For simplicity, this uses the scan order rather than enforcing
672 # sequential numbering.
673 if pk in continuations:
674 continuations[pk].append(pv)
675 else:
676 continuations[pk] = [pv]
678 # Further parts don't require their own charset marker.
679 if encoding:
680 continuation_encodings[pk] = encoding
681 else:
682 # Remove prior continuation option.
683 if pk in continuations:
684 del continuations[pk]
685 continuation_encodings.pop(pk, None)
687 options[pk] = pv
689 options.update({pk: "".join(pv) for pk, pv in continuations.items()})
690 return value, options
693_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")
696@t.overload
697def _parse_accept_header(value: str | None) -> ds.Accept: ...
700@t.overload
701def _parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ...
704def _parse_accept_header(
705 value: str | None, cls: type[_TAnyAccept] | None = None
706) -> _TAnyAccept:
707 """Parse an ``Accept`` header according to
708 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__.
710 Returns an :class:`.Accept` instance, which can sort and inspect items based on
711 their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or
712 ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.
714 :param value: The header value to parse.
715 :param cls: The :class:`.Accept` class to wrap the result in.
716 :return: An instance of ``cls``.
718 .. deprecated:: 3.2
719 Will be removed in Werkzeug 4.0. Use the ``Accept.from_header`` method
720 instead.
722 .. versionchanged:: 2.3
723 Parse according to RFC 9110. Items with invalid ``q`` values are skipped.
724 """
725 if cls is None:
726 cls = t.cast(type[_TAnyAccept], ds.Accept)
728 return cls.from_header(value)
731_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")
734@t.overload
735def _parse_cache_control_header(
736 value: str | None,
737 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
738) -> ds.RequestCacheControl: ...
741@t.overload
742def _parse_cache_control_header(
743 value: str | None,
744 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
745 cls: type[_TAnyCC] = ...,
746) -> _TAnyCC: ...
749def _parse_cache_control_header(
750 value: str | None,
751 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
752 cls: type[_TAnyCC] | None = None,
753) -> _TAnyCC:
754 """Parse a cache control header. The RFC differs between response and
755 request cache control, this method does not. It's your responsibility
756 to not use the wrong control statements.
758 .. deprecated:: 3.2
759 Will be removed in Werkzeug 4.0. Use the ``CacheControl.from_header``
760 method instead.
762 .. versionadded:: 0.5
763 The `cls` was added. If not specified an immutable
764 :class:`~werkzeug.datastructures.RequestCacheControl` is returned.
766 :param value: a cache control header to be parsed.
767 :param on_update: an optional callable that is called every time a value
768 on the :class:`~werkzeug.datastructures.CacheControl`
769 object is changed.
770 :param cls: the class for the returned object. By default
771 :class:`~werkzeug.datastructures.RequestCacheControl` is used.
772 :return: a `cls` object.
773 """
774 if cls is None:
775 cls = t.cast(type[_TAnyCC], ds.RequestCacheControl)
777 obj = cls.from_header(value)
779 if hasattr(obj, "_on_update"):
780 obj._on_update = on_update
782 return obj
785_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")
788@t.overload
789def _parse_csp_header(
790 value: str | None,
791 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
792) -> ds.ContentSecurityPolicy: ...
795@t.overload
796def _parse_csp_header(
797 value: str | None,
798 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
799 cls: type[_TAnyCSP] = ...,
800) -> _TAnyCSP: ...
803def _parse_csp_header(
804 value: str | None,
805 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
806 cls: type[_TAnyCSP] | None = None,
807) -> _TAnyCSP:
808 """Parse a Content Security Policy header.
810 .. deprecated:: 3.2
811 Will be removed in Werkzeug 4.0. Use the
812 ``ContentSecurityPolicy.from_header`` method instead.
814 .. versionadded:: 1.0
815 Support for Content Security Policy headers was added.
817 :param value: a csp header to be parsed.
818 :param on_update: an optional callable that is called every time a value
819 on the object is changed.
820 :param cls: the class for the returned object. By default
821 :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.
822 :return: a `cls` object.
823 """
824 if cls is None:
825 cls = t.cast(type[_TAnyCSP], ds.ContentSecurityPolicy)
827 obj = cls.from_header(value)
828 obj.on_update = on_update
829 return obj
832def _parse_set_header(
833 value: str | None,
834 on_update: t.Callable[[ds.HeaderSet], None] | None = None,
835) -> ds.HeaderSet:
836 """Parse a set-like header and return a
837 :class:`~werkzeug.datastructures.HeaderSet` object:
839 >>> hs = parse_set_header('token, "quoted value"')
841 The return value is an object that treats the items case-insensitively
842 and keeps the order of the items:
844 >>> 'TOKEN' in hs
845 True
846 >>> hs.index('quoted value')
847 1
848 >>> hs
849 HeaderSet(['token', 'quoted value'])
851 To create a header from the :class:`HeaderSet` again, use the
852 :func:`dump_header` function.
854 :param value: a set header to be parsed.
855 :param on_update: an optional callable that is called every time a
856 value on the :class:`~werkzeug.datastructures.HeaderSet`
857 object is changed.
859 .. deprecated:: 3.2
860 Will be removed in Werkzeug 4.0. Use the ``HeaderSet.from_header``
861 method instead.
862 """
863 obj = ds.HeaderSet.from_header(value)
864 obj._on_update = on_update
865 return obj
868def _parse_if_range_header(value: str | None) -> ds.IfRange:
869 """Parses an if-range header which can be an ETag or a date. Returns
870 a :class:`~werkzeug.datastructures.IfRange` object.
872 .. deprecated:: 3.2
873 Will be removed in Werkzeug 4.0. Use the ``IfRange.from_header``
874 method instead.
876 .. versionchanged:: 2.0
877 If the value represents a datetime, it is timezone-aware.
879 .. versionadded:: 0.7
880 """
881 return ds.IfRange.from_header(value)
884def _parse_range_header(
885 value: str | None, make_inclusive: bool = True
886) -> ds.Range | None:
887 """Parses a range header into a :class:`~werkzeug.datastructures.Range`
888 object. If the header is missing or malformed `None` is returned.
889 `ranges` is a list of ``(start, stop)`` tuples where the ranges are
890 non-inclusive.
892 .. deprecated:: 3.2
893 Will be removed in Werkzeug 4.0. Use the ``Range.from_header``
894 method instead.
896 .. versionadded:: 0.7
897 """
898 return ds.Range.from_header(value)
901def _parse_content_range_header(
902 value: str | None,
903 on_update: t.Callable[[ds.ContentRange], None] | None = None,
904) -> ds.ContentRange | None:
905 """Parses a range header into a
906 :class:`~werkzeug.datastructures.ContentRange` object or `None` if
907 parsing is not possible.
909 .. deprecated:: 3.2
910 Will be removed in Werkzeug 4.0. Use the ``ContentRange.from_header``
911 method instead.
913 .. versionadded:: 0.7
915 :param value: a content range header to be parsed.
916 :param on_update: an optional callable that is called every time a value
917 on the :class:`~werkzeug.datastructures.ContentRange`
918 object is changed.
919 """
920 if not (obj := ds.ContentRange.from_header(value)):
921 return None
923 obj._on_update = on_update
924 return obj
927def _quote_etag(etag: str, weak: bool = False) -> str:
928 """Quote an ETag value.
930 :param etag: The ETag to quote.
931 :param weak: Add a weak marker to the quoted value.
933 .. deprecated:: 3.2
934 Will be removed in Werkzeug 4.0. Use ``ETag.to_header`` instead.
935 """
936 return ds.ETag(etag, weak).to_header()
939def _unquote_etag(etag: str | None) -> tuple[str, bool] | tuple[None, None]:
940 """Parse a valid single ETag. A valid ETag must be quoted and may have an
941 optional weak ``W/`` prefix.
943 .. code-block:: pycon
945 >>> unquote_etag('"strong"')
946 ('strong', False)
947 >>> unquote_etag('W/"weak"')
948 ('weak', True)
949 >>> unquote_etag('no_quotes')
950 (None, None)
952 Use :meth:`.ETags.from_header` to parse a list of ETag values.
954 :param etag: A valid ETag to unquote.
955 :return: A tuple ``(value, weak)``, or ``(None, None)`` if the
956 value is empty or invalid.
958 .. deprecated:: 3.2
959 Will be removed in Werkzeug 4.0. Use ``ETag.from_header`` instead.
961 .. versionchanged:: 3.2
962 Does not accept invalid unquoted values.
963 """
964 if (result := ds.ETag.from_header(etag)) is None:
965 return None, None
967 return result.value, result.weak
970def _parse_etags(value: str | None) -> ds.ETagSet:
971 """Parse an ETag header.
973 :param value: the tag header to parse
974 :return: an :class:`~werkzeug.datastructures.ETags` object.
976 .. deprecated:: 3.2
977 Will be removed in Werkzeug 4.0. Use the 'ETagSet.from_header' method instead.
979 .. versionchanged:: 3.2
980 Does not accept invalid unquoted values.
981 """
982 return ds.ETagSet.from_header(value)
985def _generate_etag(data: bytes) -> str:
986 """Generate a strong ETag value by hashing the given data.
988 .. deprecated:: 3.2
989 Will be removed in Werkzeug 4.0. Use ``Response.add_etag`` instead.
991 .. versionchanged:: 3.2
992 Use SHA3-256. SHA-1 is not allowed in FIPS-enabled systems. Use base64
993 instead of hex digest. This increases the length from 40 to 43
994 characters.
996 .. versionchanged:: 2.0
997 Use SHA-1. MD5 is not allowed in FIPS-enabled systems. This increases
998 the length from 32 to 40 characters.
999 """
1000 digest = hashlib.sha3_256(data, usedforsecurity=False).digest()
1001 return b64encode(digest).decode().rstrip("=")
1004def parse_date(value: str | None) -> dt.datetime | None:
1005 """Parse an :rfc:`2822` date into a timezone-aware
1006 :class:`datetime.datetime` object, or ``None`` if parsing fails.
1008 This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It
1009 returns ``None`` if parsing fails instead of raising an exception,
1010 and always returns a timezone-aware datetime object. If the string
1011 doesn't have timezone information, it is assumed to be UTC.
1013 :param value: A string with a supported date format.
1015 .. versionchanged:: 2.0
1016 Return a timezone-aware datetime object. Use
1017 ``email.utils.parsedate_to_datetime``.
1018 """
1019 if value is None:
1020 return None
1022 try:
1023 parsed = email.utils.parsedate_to_datetime(value)
1024 except (TypeError, ValueError):
1025 return None
1027 if parsed.tzinfo is None:
1028 return parsed.replace(tzinfo=dt.UTC)
1030 return parsed
1033def http_date(
1034 timestamp: dt.datetime | dt.date | int | float | struct_time | None = None,
1035) -> str:
1036 """Format a datetime object or timestamp into an :rfc:`2822` date
1037 string.
1039 This is a wrapper for :func:`email.utils.format_datetime`. It
1040 assumes naive datetime objects are in UTC instead of raising an
1041 exception.
1043 :param timestamp: The datetime or timestamp to format. Defaults to
1044 the current time.
1046 .. versionchanged:: 2.0
1047 Use ``email.utils.format_datetime``. Accept ``date`` objects.
1048 """
1049 if isinstance(timestamp, dt.date):
1050 if not isinstance(timestamp, dt.datetime):
1051 # Assume plain date is midnight UTC.
1052 timestamp = dt.datetime.combine(timestamp, dt.time(), tzinfo=dt.UTC)
1053 else:
1054 # Ensure datetime is timezone-aware.
1055 timestamp = _dt_as_utc(timestamp)
1057 return email.utils.format_datetime(timestamp, usegmt=True)
1059 if isinstance(timestamp, struct_time):
1060 timestamp = mktime(timestamp)
1062 return email.utils.formatdate(timestamp, usegmt=True)
1065def _load_age(value: str) -> dt.timedelta | None:
1066 try:
1067 seconds = _plain_int(value)
1068 except ValueError:
1069 return None
1071 if seconds < 0:
1072 return None
1074 try:
1075 return dt.timedelta(seconds=seconds)
1076 except OverflowError:
1077 return None
1080def _dump_age(value: dt.timedelta | int) -> str:
1081 if isinstance(value, dt.timedelta):
1082 value = int(value.total_seconds())
1084 if value < 0:
1085 raise ValueError("cannot be negative")
1087 return str(value)
1090def _load_retry_after(value: str) -> dt.datetime | None:
1091 try:
1092 seconds = _plain_int(value)
1093 except ValueError:
1094 return parse_date(value)
1096 if seconds < 0:
1097 return None
1099 try:
1100 delta = dt.timedelta(seconds)
1101 except OverflowError:
1102 return None
1104 return dt.datetime.now(dt.UTC) + delta
1107def _dump_retry_after(value: dt.datetime | int) -> str:
1108 if isinstance(value, dt.datetime):
1109 return http_date(value)
1111 if value < 0:
1112 raise ValueError("cannot be negative")
1114 return str(value)
1117def _is_resource_modified(
1118 environ: wt.WSGIEnvironment,
1119 etag: str | None = None,
1120 data: bytes | None = None,
1121 last_modified: dt.datetime | str | None = None,
1122 ignore_if_range: bool = True,
1123) -> bool:
1124 """Convenience method for conditional requests.
1126 :param environ: the WSGI environment of the request to be checked.
1127 :param etag: the ETag for the response for comparison.
1128 :param data: or alternatively the data of the response to automatically
1129 generate an ETag value using :func:`generate_etag`.
1130 :param last_modified: an optional date of the last modification.
1131 :param ignore_if_range: If `False`, `If-Range` header will be taken into
1132 account.
1133 :return: `True` if the resource was modified, otherwise `False`.
1135 .. deprecated:: 3.2
1136 Will be removed in Werkzeug 4.0. ``Response.apply_conditions`` and
1137 ``apply_range`` perform their own checks.
1139 .. versionchanged:: 1.0
1140 The check is run for methods other than ``GET`` and ``HEAD``.
1141 """
1142 return _sansio_http._is_resource_modified(
1143 http_range=environ.get("HTTP_RANGE"),
1144 http_if_range=environ.get("HTTP_IF_RANGE"),
1145 http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),
1146 http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),
1147 http_if_match=environ.get("HTTP_IF_MATCH"),
1148 etag=etag,
1149 data=data,
1150 last_modified=last_modified,
1151 ignore_if_range=ignore_if_range,
1152 )
1155def _remove_entity_headers(
1156 headers: ds.Headers | list[tuple[str, str]],
1157 allowed: t.Iterable[str] = ("content-location",),
1158) -> None:
1159 """Remove all entity headers from a list or :class:`Headers` object. This
1160 operation works in-place.
1162 :param headers: a list or :class:`Headers` object.
1163 :param allowed: a list of headers that should still be allowed even though
1164 they are entity headers.
1166 .. deprecated:: 3.2
1167 Will be removed in Werkzeug 4.0. Use ``Response`` instead.
1169 .. versionchanged:: 3.2
1170 The list of headers is updated to RFC 9110.
1172 .. versionchanged:: 0.5
1173 The ``allowed`` parameter was added.
1175 """
1176 allowed = {x.lower() for x in allowed}
1177 headers[:] = [
1178 (key, value)
1179 for key, value in headers
1180 if not _is_entity_header(key) or key.lower() in allowed
1181 ]
1184def _remove_hop_by_hop_headers(
1185 headers: ds.Headers | list[tuple[str, str]], connection: ds.HeaderSet | None = None
1186) -> None:
1187 """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or
1188 :class:`Headers` object. This operation works in-place.
1190 :param headers: a list or :class:`Headers` object.
1191 :param connection: The parsed ``Connection`` header.
1193 .. deprecated:: 3.2
1194 Will be removed in Werkzeug 4.0. Use ``is_hop_by_hop_header`` to filter
1195 while building a new ``Headers`` object.
1197 .. versionchanged:: 3.2
1198 Accepts a parsed ``Connection`` header to check against. The list of
1199 headers to always remove is updated to RFC 9110.
1201 .. versionadded:: 0.5
1202 """
1203 headers[:] = [
1204 (key, value)
1205 for key, value in headers
1206 if not is_hop_by_hop_header(key, connection)
1207 ]
1210def _is_entity_header(header: str) -> bool:
1211 """Check if a header is an entity header.
1213 This is defined in https://httpwg.org/specs/rfc9110.html#status.304 and
1214 https://httpwg.org/specs/rfc9110.html#representation.metadata.
1216 :param header: The header key to check.
1218 .. deprecated:: 3.2
1219 Will be removed in Werkzeug 4.0. Use ``Response`` instead.
1221 .. versionchanged:: 3.2
1222 The list of headers is updated to RFC 9110.
1224 .. versionadded:: 0.5
1225 """
1226 return header.lower() in {
1227 "content-type",
1228 "content-encoding",
1229 "content-language",
1230 "content-length",
1231 }
1234_hop_by_hop_headers = frozenset(
1235 (
1236 "connection",
1237 "proxy-connection",
1238 "keep-alive",
1239 "te",
1240 "transfer-encoding",
1241 "upgrade",
1242 )
1243)
1246def is_hop_by_hop_header(key: str, connection: ds.HeaderSet | None = None) -> bool:
1247 """Check if a header should be removed by a proxy.
1249 This is defined in https://httpwg.org/specs/rfc9110.html#field.connection.
1250 All headers to remove should be listed in the ``Connection`` header. In case
1251 it's not given, there is a set of headers that is always removed.
1253 :param key: The header key to check.
1254 :param connection: The parsed ``Connection`` header.
1256 .. versionchanged:: 3.2
1257 Accepts a parsed ``Connection`` header to check against. The list of
1258 headers to always remove is updated to RFC 9110.
1260 .. versionadded:: 0.5
1261 """
1262 key = key.lower()
1263 return key in _hop_by_hop_headers or (connection is not None and key in connection)
1266def parse_cookie(
1267 header: wt.WSGIEnvironment | str | None, **kwargs: t.Any
1268) -> ds.ImmutableMultiDict[str, str]:
1269 """Parse cookies from a ``Cookie`` header as an :class:`.ImmutableMultiDict`.
1271 :param header: The ``Cookie`` header, or a WSGI environ with an
1272 ``HTTP_COOKIE`` key.
1274 .. versionchanged:: 3.2
1275 The ``cls`` parameter is deprecated and will be removed in Werkzeug 4.0.
1276 It will always be ``ImmutableMultiDict``.
1278 .. versionchanged:: 3.0
1279 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
1281 .. versionchanged:: 1.0
1282 Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.
1284 .. versionchanged:: 0.5
1285 Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls``
1286 parameter was added.
1287 """
1288 if isinstance(header, dict):
1289 cookie = header.get("HTTP_COOKIE")
1290 else:
1291 cookie = header
1293 if cookie:
1294 cookie = cookie.encode("latin1").decode()
1296 parse_kwargs: dict[str, t.Any] = {}
1298 if "cls" in kwargs:
1299 import warnings
1301 warnings.warn(
1302 "The 'cls' parameter is deprecated and will be removed in Werkzeug"
1303 " 4.0. It will always be 'ImmutableMultiDict'.",
1304 DeprecationWarning,
1305 stacklevel=2,
1306 )
1307 parse_kwargs["cls"] = kwargs["cls"]
1309 return _sansio_http.parse_cookie(cookie=cookie, **kwargs)
1312_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
1313_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)
1314_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}
1315_cookie_slash_map.update(
1316 (v.to_bytes(1, "big"), b"\\%03o" % v)
1317 for v in [*range(0x20), *b",;", *range(0x7F, 256)]
1318)
1321def dump_cookie(
1322 key: str,
1323 value: str = "",
1324 max_age: dt.timedelta | int | None = None,
1325 expires: str | dt.datetime | int | float | None = None,
1326 path: str | None = "/",
1327 domain: str | None = None,
1328 secure: bool = False,
1329 httponly: bool = False,
1330 sync_expires: bool = True,
1331 max_size: int = 4093,
1332 samesite: str | None = None,
1333 partitioned: bool = False,
1334) -> str:
1335 """Create a Set-Cookie header without the ``Set-Cookie`` prefix.
1337 The return value is usually restricted to ascii as the vast majority
1338 of values are properly escaped, but that is no guarantee. It's
1339 tunneled through latin1 as required by :pep:`3333`.
1341 The return value is not ASCII safe if the key contains unicode
1342 characters. This is technically against the specification but
1343 happens in the wild. It's strongly recommended to not use
1344 non-ASCII values for the keys.
1346 :param key: The cookie key. This is assumed to be trusted and valid, it
1347 must not come from untrusted user input.
1348 :param value: The cookie value. It will be quoted if it contains characters
1349 not allowed by RFC 6265; ``\123`` octal escapes are used to encode
1350 non-ASCII bytes.
1351 :param max_age: should be a number of seconds, or `None` (default) if
1352 the cookie should last only as long as the client's
1353 browser session. Additionally `timedelta` objects
1354 are accepted, too.
1355 :param expires: should be a `datetime` object or unix timestamp.
1356 :param path: limits the cookie to a given path, per default it will
1357 span the whole domain.
1358 :param domain: Use this if you want to set a cross-domain cookie. For
1359 example, ``domain="example.com"`` will set a cookie
1360 that is readable by the domain ``www.example.com``,
1361 ``foo.example.com`` etc. Otherwise, a cookie will only
1362 be readable by the domain that set it.
1363 :param secure: The cookie will only be available via HTTPS
1364 :param httponly: disallow JavaScript to access the cookie. This is an
1365 extension to the cookie standard and probably not
1366 supported by all browsers.
1367 :param charset: the encoding for string values.
1368 :param sync_expires: automatically set expires if max_age is defined
1369 but expires not.
1370 :param max_size: Warn if the final header value exceeds this size. The
1371 default, 4093, should be safely `supported by most browsers
1372 <cookie_>`_. Set to 0 to disable this check.
1373 :param samesite: Limits the scope of the cookie such that it will
1374 only be attached to requests if those requests are same-site.
1375 :param partitioned: Opts the cookie into partitioned storage. This
1376 will also set secure to True
1378 .. _`cookie`: http://browsercookielimits.squawky.net/
1380 .. versionchanged:: 3.1
1381 The ``partitioned`` parameter was added.
1383 .. versionchanged:: 3.0
1384 Passing bytes, and the ``charset`` parameter, were removed.
1386 .. versionchanged:: 2.3.3
1387 The ``path`` parameter is ``/`` by default.
1389 .. versionchanged:: 2.3.1
1390 The value allows more characters without quoting.
1392 .. versionchanged:: 2.3
1393 ``localhost`` and other names without a dot are allowed for the domain. A
1394 leading dot is ignored.
1396 .. versionchanged:: 2.3
1397 The ``path`` parameter is ``None`` by default.
1399 .. versionchanged:: 1.0.0
1400 The string ``'None'`` is accepted for ``samesite``.
1401 """
1402 if path is not None:
1403 # safe = https://url.spec.whatwg.org/#url-path-segment-string
1404 # as well as percent for things that are already quoted
1405 # excluding semicolon since it's part of the header syntax
1406 path = quote(path, safe="%!$&'()*+,/:=@")
1408 if domain:
1409 domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii")
1411 if isinstance(max_age, dt.timedelta):
1412 max_age = int(max_age.total_seconds())
1414 if expires is not None:
1415 if not isinstance(expires, str):
1416 expires = http_date(expires)
1417 elif max_age is not None and sync_expires:
1418 expires = http_date(dt.datetime.now(tz=dt.UTC).timestamp() + max_age)
1420 if samesite is not None:
1421 samesite = samesite.title()
1423 if samesite not in {"Strict", "Lax", "None"}:
1424 raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
1426 if partitioned:
1427 secure = True
1429 # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
1430 # three octal digits, which matches http.cookies, although the RFC suggests base64.
1431 if not _cookie_no_quote_re.fullmatch(value):
1432 # Work with bytes here, since a UTF-8 character could be multiple bytes.
1433 value = _cookie_slash_re.sub(
1434 lambda m: _cookie_slash_map[m.group()], value.encode()
1435 ).decode("ascii")
1436 value = f'"{value}"'
1438 # Send a non-ASCII key as mojibake. Everything else should already be ASCII.
1439 # TODO Remove encoding dance, it seems like clients accept UTF-8 keys
1440 buf = [f"{key.encode().decode('latin1')}={value}"]
1442 for k, v in (
1443 ("Domain", domain),
1444 ("Expires", expires),
1445 ("Max-Age", max_age),
1446 ("Secure", secure),
1447 ("HttpOnly", httponly),
1448 ("Path", path),
1449 ("SameSite", samesite),
1450 ("Partitioned", partitioned),
1451 ):
1452 if v is None or v is False:
1453 continue
1455 if v is True:
1456 buf.append(k)
1457 continue
1459 buf.append(f"{k}={v}")
1461 rv = "; ".join(buf)
1463 # Warn if the final value of the cookie is larger than the limit. If the cookie is
1464 # too large, then it may be silently ignored by the browser, which can be quite hard
1465 # to debug.
1466 cookie_size = len(rv)
1468 if max_size and cookie_size > max_size:
1469 value_size = len(value)
1470 warnings.warn(
1471 f"The '{key}' cookie is too large: the value was {value_size} bytes but the"
1472 f" header required {cookie_size - value_size} extra bytes. The final size"
1473 f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may"
1474 " silently ignore cookies larger than this.",
1475 stacklevel=2,
1476 )
1478 return rv
1481def _is_byte_range_valid(
1482 start: int | None, stop: int | None, length: int | None
1483) -> bool:
1484 """Checks if a given byte content range is valid for the given length.
1486 .. deprecated:: 3.2
1487 Will be removed in Werkzeug 4.0. ``Range.from_header``,
1488 ``Range.make_content_range``, and ``ContentRange.from_header`` validate
1489 their values.
1491 .. versionadded:: 0.7
1492 """
1493 if (start is None) != (stop is None):
1494 return False
1495 elif start is None:
1496 return length is None or length >= 0
1497 elif length is None:
1498 return 0 <= start < stop # type: ignore[operator]
1499 elif start >= stop: # type: ignore[operator]
1500 return False
1501 return 0 <= start < length
1504# circular dependencies
1505from . import datastructures as ds # noqa: E402
1506from .sansio import http as _sansio_http # noqa: E402
1508if not t.TYPE_CHECKING:
1510 def __getattr__(name: str) -> t.Any:
1511 import warnings
1513 if name == "HTTP_STATUS_CODES":
1514 warnings.warn(
1515 "The 'HTTP_STATUS_CODES' data is deprecated and will be removed in"
1516 " Werkzeug 4.0. Use Python's built-in 'http.HTTPStatus' instead.",
1517 DeprecationWarning,
1518 stacklevel=2,
1519 )
1520 return _HTTP_STATUS_CODES
1522 if name == "is_resource_modified":
1523 warnings.warn(
1524 "The 'is_resource_modified' function is deprecated and will be removed"
1525 " in Werkzeug 4.0. 'Response.apply_conditions' and"
1526 " 'Response.apply_range' perform their own checks.",
1527 DeprecationWarning,
1528 stacklevel=2,
1529 )
1530 return _is_resource_modified
1532 if name == "is_byte_range_valid":
1533 warnings.warn(
1534 "The 'is_byte_range_valid' function is deprecated and will be"
1535 " removed in Werkzeug 4.0. 'Range.from_header',"
1536 " 'Range.make_content_range', and 'ContentRange.from_header'"
1537 " validate their values.",
1538 DeprecationWarning,
1539 stacklevel=2,
1540 )
1541 return _is_byte_range_valid
1543 if name == "generate_etag":
1544 warnings.warn(
1545 "The 'generate_etag' function is deprecated and will be removed in"
1546 " Werkzeug 4.0. Use 'Response.add_etag' instead.",
1547 DeprecationWarning,
1548 stacklevel=2,
1549 )
1550 return _generate_etag
1552 if name == "remove_hop_by_hop_headers":
1553 warnings.warn(
1554 "The 'remove_hop_by_hop_headers' function is deprecated and will be"
1555 " removed in Werkzeug 4.0. Use 'is_hop_by_hop_header' to filter while"
1556 " building a new 'Headers' object.",
1557 DeprecationWarning,
1558 stacklevel=2,
1559 )
1560 return _remove_hop_by_hop_headers
1562 if name == "remove_entity_headers":
1563 warnings.warn(
1564 "The 'remove_entity_headers' function is deprecated and will be removed"
1565 " in Werkzeug 4.0. Use 'Response' instead.",
1566 DeprecationWarning,
1567 stacklevel=2,
1568 )
1569 return _remove_entity_headers
1571 if name == "is_entity_header":
1572 warnings.warn(
1573 "The 'is_entity_header' function is deprecated and will be removed in"
1574 " Werkzeug 4.0. Use 'Response' instead.",
1575 DeprecationWarning,
1576 stacklevel=2,
1577 )
1578 return _is_entity_header
1580 alts = {
1581 "dump_csp_header": "ContentSecurityPolicy.to_header",
1582 "parse_accept_header": "Accept.from_header",
1583 "parse_cache_control_header": "CacheControl.from_header",
1584 "parse_content_range_header": "ContentRange.from_header",
1585 "parse_csp_header": "ContentSecurityPolicy.from_header",
1586 "parse_etags": "ETagSet.from_header",
1587 "parse_if_range_header": "IfRange.from_header",
1588 "parse_range_header": "Range.from_header",
1589 "parse_set_header": "HeaderSet.from_header",
1590 "unquote_etag": "ETag.from_header",
1591 "quote_etag": "ETag.to_header",
1592 }
1594 if name in alts:
1595 warnings.warn(
1596 f"The '{name}' function is deprecated and will be removed in"
1597 f" Werkzeug 4.0. Use the '{alts[name]}' method instead.",
1598 DeprecationWarning,
1599 stacklevel=2,
1600 )
1601 return globals()[f"_{name}"]
1603 raise AttributeError(name)