Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/http.py: 65%
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 email.utils
4import hashlib
5import re
6import typing as t
7import warnings
8from base64 import b64encode
9from datetime import date
10from datetime import datetime
11from datetime import time
12from datetime import timedelta
13from datetime import timezone
14from enum import Enum
15from time import mktime
16from time import struct_time
17from urllib.parse import quote
18from urllib.parse import unquote
20from ._internal import _dt_as_utc
21from ._internal import _plain_int
23if t.TYPE_CHECKING:
24 from _typeshed.wsgi import WSGIEnvironment
26_token_chars = frozenset(
27 "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
28)
29_entity_headers = frozenset(
30 [
31 "allow",
32 "content-encoding",
33 "content-language",
34 "content-length",
35 "content-location",
36 "content-md5",
37 "content-range",
38 "content-type",
39 "expires",
40 "last-modified",
41 ]
42)
43_hop_by_hop_headers = frozenset(
44 [
45 "connection",
46 "keep-alive",
47 "proxy-authenticate",
48 "proxy-authorization",
49 "te",
50 "trailer",
51 "transfer-encoding",
52 "upgrade",
53 ]
54)
55_HTTP_STATUS_CODES = {
56 100: "Continue",
57 101: "Switching Protocols",
58 102: "Processing",
59 103: "Early Hints", # see RFC 8297
60 200: "OK",
61 201: "Created",
62 202: "Accepted",
63 203: "Non Authoritative Information",
64 204: "No Content",
65 205: "Reset Content",
66 206: "Partial Content",
67 207: "Multi Status",
68 208: "Already Reported", # see RFC 5842
69 226: "IM Used", # see RFC 3229
70 300: "Multiple Choices",
71 301: "Moved Permanently",
72 302: "Found",
73 303: "See Other",
74 304: "Not Modified",
75 305: "Use Proxy",
76 306: "Switch Proxy", # unused
77 307: "Temporary Redirect",
78 308: "Permanent Redirect",
79 400: "Bad Request",
80 401: "Unauthorized",
81 402: "Payment Required", # unused
82 403: "Forbidden",
83 404: "Not Found",
84 405: "Method Not Allowed",
85 406: "Not Acceptable",
86 407: "Proxy Authentication Required",
87 408: "Request Timeout",
88 409: "Conflict",
89 410: "Gone",
90 411: "Length Required",
91 412: "Precondition Failed",
92 413: "Request Entity Too Large",
93 414: "Request URI Too Long",
94 415: "Unsupported Media Type",
95 416: "Requested Range Not Satisfiable",
96 417: "Expectation Failed",
97 418: "I'm a teapot", # see RFC 2324
98 421: "Misdirected Request", # see RFC 7540
99 422: "Unprocessable Entity",
100 423: "Locked",
101 424: "Failed Dependency",
102 425: "Too Early", # see RFC 8470
103 426: "Upgrade Required",
104 428: "Precondition Required", # see RFC 6585
105 429: "Too Many Requests",
106 431: "Request Header Fields Too Large",
107 449: "Retry With", # proprietary MS extension
108 451: "Unavailable For Legal Reasons",
109 500: "Internal Server Error",
110 501: "Not Implemented",
111 502: "Bad Gateway",
112 503: "Service Unavailable",
113 504: "Gateway Timeout",
114 505: "HTTP Version Not Supported",
115 506: "Variant Also Negotiates", # see RFC 2295
116 507: "Insufficient Storage",
117 508: "Loop Detected", # see RFC 5842
118 510: "Not Extended",
119 511: "Network Authentication Failed",
120}
123class COEP(Enum):
124 """``Cross-Origin-Embedder-Policy`` header values. Used by
125 :attr:`.Response.cross_origin_embedder_policy`.
127 .. versionchanged:: 3.2
128 Added the ``credentialless`` member.
130 .. versionadded:: 2.0
131 """
133 UNSAFE_NONE = "unsafe-none"
134 REQUIRE_CORP = "require-corp"
135 CREDENTIALLESS = "credentialless"
138class COOP(Enum):
139 """``Cross-Origin-Opener-Policy`` header values. Used by
140 :attr:`.Response.cross_origin_opener_policy`.
142 .. versionchanged:: 3.2
143 Added the ``noopener-allow-popups`` member.
145 .. versionadded:: 2.0
146 """
148 UNSAFE_NONE = "unsafe-none"
149 SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"
150 SAME_ORIGIN = "same-origin"
151 NOOPENER_ALLOW_POPUPS = "noopener-allow-popups"
154class CORP(Enum):
155 """``Cross-Origin-Resource-Policy`` header values. Used by
156 :attr:`.Response.cross_origin_resource_policy`.
158 .. versionadded:: 3.2
159 """
161 SAME_SITE = "same-site"
162 SAME_ORIGIN = "same-origin"
163 CROSS_ORIGIN = "cross-origin"
166class SecFetchSite(Enum):
167 """``Sec-Fetch-Site`` header values. Used by :attr:`.Request.sec_fetch_site`.
169 .. versionadded:: 3.2
170 """
172 CROSS_SITE = "cross-site"
173 SAME_ORIGIN = "same-origin"
174 SAME_SITE = "same-site"
175 NONE = "none"
178class SecFetchMode(Enum):
179 """``Sec-Fetch-Mode`` header values. Used by :attr:`.Request.sec_fetch_mode`.
181 .. versionadded:: 3.2
182 """
184 CORS = "cors"
185 NAVIGATE = "navigate"
186 NO_CORS = "no-cors"
187 SAME_ORIGIN = "same-origin"
188 WEBSOCKET = "websocket"
191class SecFetchDest(Enum):
192 """``Sec-Fetch-Dest`` header values. Used by :attr:`.Request.sec_fetch_dest`.
194 .. versionadded:: 3.2
195 """
197 AUDIO = "audio"
198 AUDIOWORKLET = "audioworklet"
199 DOCUMENT = "document"
200 EMBED = "embed"
201 EMPTY = "empty"
202 FENCEDFRAME = "fencedframe"
203 FONT = "font"
204 FRAME = "frame"
205 IFRAME = "iframe"
206 IMAGE = "image"
207 JSON = "json"
208 MANIFEST = "manifest"
209 OBJECT = "object"
210 PAINTWORKLET = "paintworklet"
211 REPORT = "report"
212 SCRIPT = "script"
213 SERVICEWORKER = "serviceworker"
214 SHAREDWORKER = "sharedworker"
215 STYLE = "style"
216 TRACK = "track"
217 VIDEO = "video"
218 WEBIDENTITY = "webidentity"
219 WORKER = "worker"
220 XSLT = "xslt"
223def quote_header_value(value: t.Any, allow_token: bool = True) -> str:
224 """Add double quotes around a header value. If the header contains only ASCII token
225 characters, it will be returned unchanged. If the header contains ``"`` or ``\\``
226 characters, they will be escaped with an additional ``\\`` character.
228 This is the reverse of :func:`unquote_header_value`.
230 :param value: The value to quote. Will be converted to a string.
231 :param allow_token: Disable to quote the value even if it only has token characters.
233 .. versionchanged:: 3.0
234 Passing bytes is not supported.
236 .. versionchanged:: 3.0
237 The ``extra_chars`` parameter is removed.
239 .. versionchanged:: 2.3
240 The value is quoted if it is the empty string.
242 .. versionadded:: 0.5
243 """
244 value_str = str(value)
246 if not value_str:
247 return '""'
249 if allow_token:
250 token_chars = _token_chars
252 if token_chars.issuperset(value_str):
253 return value_str
255 value_str = value_str.replace("\\", "\\\\").replace('"', '\\"')
256 return f'"{value_str}"'
259_unslash_re = re.compile(r"\\(.)", re.A)
262def unquote_header_value(value: str) -> str:
263 """Remove double quotes and backslash escapes from a header value.
265 This is the reverse of :func:`quote_header_value`.
267 :param value: The header value to unquote.
269 .. versionchanged:: 3.2
270 Removes escape preceding any character.
272 .. versionchanged:: 3.0
273 The ``is_filename`` parameter is removed.
274 """
275 if len(value) >= 2 and value[0] == value[-1] == '"':
276 return _unslash_re.sub(r"\g<1>", value[1:-1])
278 return value
281def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:
282 """Produce a header value and ``key=value`` parameters separated by semicolons
283 ``;``. For example, the ``Content-Type`` header.
285 .. code-block:: python
287 dump_options_header("text/html", {"charset": "UTF-8"})
288 'text/html; charset=UTF-8'
290 This is the reverse of :func:`parse_options_header`.
292 If a value contains non-token characters, it will be quoted.
294 If a value is ``None``, the parameter is skipped.
296 In some keys for some headers, a UTF-8 value can be encoded using a special
297 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
298 not produce that format automatically, but if a given key ends with an asterisk
299 ``*``, the value is assumed to have that form and will not be quoted further.
301 :param header: The primary header value.
302 :param options: Parameters to encode as ``key=value`` pairs.
304 .. versionchanged:: 2.3
305 Keys with ``None`` values are skipped rather than treated as a bare key.
307 .. versionchanged:: 2.2.3
308 If a key ends with ``*``, its value will not be quoted.
309 """
310 segments = []
312 if header is not None:
313 segments.append(header)
315 for key, value in options.items():
316 if value is None:
317 continue
319 if key[-1] == "*":
320 segments.append(f"{key}={value}")
321 else:
322 segments.append(f"{key}={quote_header_value(value)}")
324 return "; ".join(segments)
327def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str:
328 """Produce a header value from a list of items or ``key=value`` pairs, separated by
329 commas ``,``.
331 This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and
332 :func:`parse_set_header`.
334 If a value contains non-token characters, it will be quoted.
336 If a value is ``None``, the key is output alone.
338 In some keys for some headers, a UTF-8 value can be encoded using a special
339 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
340 not produce that format automatically, but if a given key ends with an asterisk
341 ``*``, the value is assumed to have that form and will not be quoted further.
343 .. code-block:: python
345 dump_header(["foo", "bar baz"])
346 'foo, "bar baz"'
348 dump_header({"foo": "bar baz"})
349 'foo="bar baz"'
351 :param iterable: The items to create a header from.
353 .. versionchanged:: 3.0
354 The ``allow_token`` parameter is removed.
356 .. versionchanged:: 2.2.3
357 If a key ends with ``*``, its value will not be quoted.
358 """
359 if isinstance(iterable, dict):
360 items = []
362 for key, value in iterable.items():
363 if value is None:
364 items.append(key)
365 elif key[-1] == "*":
366 items.append(f"{key}={value}")
367 else:
368 items.append(f"{key}={quote_header_value(value)}")
369 else:
370 items = [quote_header_value(x) for x in iterable]
372 return ", ".join(items)
375def _dump_csp_header(header: ds.ContentSecurityPolicy) -> str:
376 """Dump a Content Security Policy header.
378 These are structured into policies such as "default-src 'self';
379 script-src 'self'".
381 .. deprecated:: 3.2
382 Will be removed in Werkzeug 3.3. Use the
383 ``ContentSecurityPolicy.to_header`` method instead.
385 .. versionadded:: 1.0.0
386 Support for Content Security Policy headers was added.
388 """
389 import warnings
391 warnings.warn(
392 "The 'dump_csp_header' function is deprecated and will be removed in"
393 " Werkzeug 3.3. Use the 'ContentSecurityPolicy.to_header' method instead.",
394 DeprecationWarning,
395 stacklevel=2,
396 )
397 return header.to_header()
400def parse_list_header(value: str) -> list[str]:
401 """Parse a header value that consists of a list of comma separated items according
402 to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.
404 Surrounding quotes are removed from items, but internal quotes are left for
405 future parsing. Empty values are discarded.
407 .. code-block:: python
409 parse_list_header('token, "quoted value"')
410 ['token', 'quoted value']
412 This is the reverse of :func:`dump_header`.
414 :param value: The header value to parse.
416 .. versionchanged:: 3.2
417 Quotes and escapes are kept if only part of an item is quoted. Empty
418 values are omitted. An empty list is returned if the value contains an
419 unclosed quoted string.
420 """
421 items = []
422 item = ""
423 escape = False
424 quote = False
426 for char in value:
427 if escape:
428 escape = False
429 item += char
430 continue
432 if quote:
433 if char == "\\":
434 escape = True
435 elif char == '"':
436 quote = False
438 item += char
439 continue
441 if char == ",":
442 items.append(item)
443 item = ""
444 continue
446 if char == '"':
447 quote = True
449 item += char
451 if quote:
452 # invalid, unclosed quoted string
453 return []
455 items.append(item)
456 return [
457 unquote_header_value(item) for item in (item.strip() for item in items) if item
458 ]
461def parse_dict_header(value: str) -> dict[str, str | None]:
462 """Parse a list header using :func:`parse_list_header`, then parse each item as a
463 ``key=value`` pair.
465 .. code-block:: python
467 parse_dict_header('a=b, c="d, e", f')
468 {"a": "b", "c": "d, e", "f": None}
470 This is the reverse of :func:`dump_header`.
472 If a key does not have a value, it is ``None``.
474 This handles charsets for values as described in
475 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,
476 and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.
478 :param value: The header value to parse.
480 .. versionchanged:: 3.2
481 An empty dict is returned if the value contains an unclosed quoted
482 string.
484 .. versionchanged:: 3.0
485 Passing bytes is not supported.
487 .. versionchanged:: 3.0
488 The ``cls`` argument is removed.
490 .. versionchanged:: 2.3
491 Added support for ``key*=charset''value`` encoded items.
493 .. versionchanged:: 0.9
494 The ``cls`` argument was added.
495 """
496 result: dict[str, str | None] = {}
498 for item in parse_list_header(value):
499 key, has_value, value = item.partition("=")
500 key = key.strip()
502 if not key:
503 # =value is not valid
504 continue
506 if not has_value:
507 result[key] = None
508 continue
510 value = value.strip()
511 encoding: str | None = None
513 if key[-1] == "*":
514 # key*=charset''value becomes key=value, where value is percent encoded
515 # adapted from parse_options_header, without the continuation handling
516 key = key[:-1]
517 match = _charset_value_re.match(value)
519 if match:
520 # If there is a charset marker in the value, split it off.
521 encoding, value = match.groups()
522 encoding = encoding.lower()
524 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
525 # This list will not be extended further. An invalid encoding will leave the
526 # value quoted.
527 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
528 # invalid bytes are replaced during unquoting
529 value = unquote(value, encoding=encoding)
531 result[key] = unquote_header_value(value)
533 return result
536# https://httpwg.org/specs/rfc9110.html#parameter
537_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII)
538_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII)
539# https://www.rfc-editor.org/rfc/rfc2231#section-4
540_charset_value_re = re.compile(
541 r"""
542 ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty
543 [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty
544 ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding
545 """,
546 re.ASCII | re.VERBOSE,
547)
548# https://www.rfc-editor.org/rfc/rfc2231#section-3
549_continuation_re = re.compile(r"\*(\d+)$", re.ASCII)
552def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
553 """Parse a header that consists of a value with ``key=value`` parameters separated
554 by semicolons ``;``. For example, the ``Content-Type`` header.
556 .. code-block:: python
558 parse_options_header("text/html; charset=UTF-8")
559 ('text/html', {'charset': 'UTF-8'})
561 parse_options_header("")
562 ("", {})
564 This is the reverse of :func:`dump_options_header`.
566 This parses valid parameter parts as described in
567 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are
568 skipped.
570 This handles continuations and charsets as described in
571 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as
572 strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,
573 otherwise the value remains quoted.
575 Clients may not be consistent in how they handle a quote character within a quoted
576 value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__
577 replaces it with ``%22`` in multipart form data.
578 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash
579 escapes in HTTP headers. Both are decoded to the ``"`` character.
581 Clients may not be consistent in how they handle non-ASCII characters. HTML
582 documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with
583 HTML character references, which can be decoded using :func:`html.unescape`.
585 :param value: The header value to parse.
586 :return: ``(value, options)``, where ``options`` is a dict
588 .. versionchanged:: 2.3
589 Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted
590 values, are discarded instead of treating as ``None``.
592 .. versionchanged:: 2.3
593 Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.
595 .. versionchanged:: 2.3
596 Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.
598 .. versionchanged:: 2.2
599 Option names are always converted to lowercase.
601 .. versionchanged:: 2.2
602 The ``multiple`` parameter was removed.
604 .. versionchanged:: 0.15
605 :rfc:`2231` parameter continuations are handled.
607 .. versionadded:: 0.5
608 """
609 if value is None:
610 return "", {}
612 value, _, rest = value.partition(";")
613 value = value.strip()
614 rest = rest.strip()
616 if not value or not rest:
617 # empty (invalid) value, or value without options
618 return value, {}
620 # Collect all valid key=value parts without processing the value.
621 parts: list[tuple[str, str]] = []
623 while True:
624 if (m := _parameter_key_re.match(rest)) is not None:
625 pk = m.group(1).lower()
626 rest = rest[m.end() :]
628 # Value may be a token.
629 if (m := _parameter_token_value_re.match(rest)) is not None:
630 parts.append((pk, m.group()))
632 # Value may be a quoted string, find the closing quote.
633 elif rest[:1] == '"':
634 pos = 1
635 length = len(rest)
637 while pos < length:
638 if rest[pos : pos + 2] in {"\\\\", '\\"'}:
639 # Consume escaped slashes and quotes.
640 pos += 2
641 elif rest[pos] == '"':
642 # Stop at an unescaped quote.
643 parts.append((pk, rest[: pos + 1]))
644 rest = rest[pos + 1 :]
645 break
646 else:
647 # Consume any other character.
648 pos += 1
650 # Find the next section delimited by `;`, if any.
651 if (end := rest.find(";")) == -1:
652 break
654 rest = rest[end + 1 :].lstrip()
656 options: dict[str, str] = {}
657 encoding: str | None = None
658 continued_encoding: str | None = None
660 # For each collected part, process optional charset and continuation,
661 # unquote quoted values.
662 for pk, pv in parts:
663 if pk[-1] == "*":
664 # key*=charset''value becomes key=value, where value is percent encoded
665 pk = pk[:-1]
666 match = _charset_value_re.match(pv)
668 if match:
669 # If there is a valid charset marker in the value, split it off.
670 encoding, pv = match.groups()
671 # This might be the empty string, handled next.
672 encoding = encoding.lower()
674 # No charset marker, or marker with empty charset value.
675 if not encoding:
676 encoding = continued_encoding
678 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
679 # This list will not be extended further. An invalid encoding will leave the
680 # value quoted.
681 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
682 # Continuation parts don't require their own charset marker. This is
683 # looser than the RFC, it will persist across different keys and allows
684 # changing the charset during a continuation. But this implementation is
685 # much simpler than tracking the full state.
686 continued_encoding = encoding
687 # invalid bytes are replaced during unquoting
688 pv = unquote(pv, encoding=encoding)
690 # Remove quotes. At this point the value cannot be empty or a single quote.
691 if pv[0] == pv[-1] == '"':
692 # HTTP headers use slash, multipart form data uses percent
693 pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"')
695 match = _continuation_re.search(pk)
697 if match:
698 # key*0=a; key*1=b becomes key=ab
699 pk = pk[: match.start()]
700 options[pk] = options.get(pk, "") + pv
701 else:
702 options[pk] = pv
704 return value, options
707_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")
710@t.overload
711def _parse_accept_header(value: str | None) -> ds.Accept: ...
714@t.overload
715def _parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ...
718def _parse_accept_header(
719 value: str | None, cls: type[_TAnyAccept] | None = None
720) -> _TAnyAccept:
721 """Parse an ``Accept`` header according to
722 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__.
724 Returns an :class:`.Accept` instance, which can sort and inspect items based on
725 their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or
726 ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.
728 :param value: The header value to parse.
729 :param cls: The :class:`.Accept` class to wrap the result in.
730 :return: An instance of ``cls``.
732 .. deprecated:: 3.2
733 Will be removed in Werkzeug 3.3. Use the ``Accept.from_header`` method
734 instead.
736 .. versionchanged:: 2.3
737 Parse according to RFC 9110. Items with invalid ``q`` values are skipped.
738 """
739 import warnings
741 warnings.warn(
742 "The 'parse_accept_header' function is deprecated and will be removed in"
743 " Werkzeug 3.3. Use the 'Accept.from_header' method instead.",
744 DeprecationWarning,
745 stacklevel=2,
746 )
748 if cls is None:
749 cls = t.cast(type[_TAnyAccept], ds.Accept)
751 return cls.from_header(value)
754_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")
757@t.overload
758def _parse_cache_control_header(
759 value: str | None,
760 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
761) -> ds.RequestCacheControl: ...
764@t.overload
765def _parse_cache_control_header(
766 value: str | None,
767 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
768 cls: type[_TAnyCC] = ...,
769) -> _TAnyCC: ...
772def _parse_cache_control_header(
773 value: str | None,
774 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
775 cls: type[_TAnyCC] | None = None,
776) -> _TAnyCC:
777 """Parse a cache control header. The RFC differs between response and
778 request cache control, this method does not. It's your responsibility
779 to not use the wrong control statements.
781 .. deprecated:: 3.2
782 Will be removed in Werkzeug 3.3. Use the ``CacheControl.from_header``
783 method instead.
785 .. versionadded:: 0.5
786 The `cls` was added. If not specified an immutable
787 :class:`~werkzeug.datastructures.RequestCacheControl` is returned.
789 :param value: a cache control header to be parsed.
790 :param on_update: an optional callable that is called every time a value
791 on the :class:`~werkzeug.datastructures.CacheControl`
792 object is changed.
793 :param cls: the class for the returned object. By default
794 :class:`~werkzeug.datastructures.RequestCacheControl` is used.
795 :return: a `cls` object.
796 """
797 import warnings
799 warnings.warn(
800 "The 'parse_cache_control_header' function is deprecated and will be"
801 " removed in Werkzeug 3.3. Use the 'RequestCacheControl.from_header'"
802 " method instead.",
803 DeprecationWarning,
804 stacklevel=2,
805 )
807 if cls is None:
808 cls = t.cast(type[_TAnyCC], ds.RequestCacheControl)
810 obj = cls.from_header(value)
811 obj.on_update = on_update
812 return obj
815_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")
818@t.overload
819def _parse_csp_header(
820 value: str | None,
821 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
822) -> ds.ContentSecurityPolicy: ...
825@t.overload
826def _parse_csp_header(
827 value: str | None,
828 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
829 cls: type[_TAnyCSP] = ...,
830) -> _TAnyCSP: ...
833def _parse_csp_header(
834 value: str | None,
835 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
836 cls: type[_TAnyCSP] | None = None,
837) -> _TAnyCSP:
838 """Parse a Content Security Policy header.
840 .. deprecated:: 3.2
841 Will be removed in Werkzeug 3.3. Use the
842 ``ContentSecurityPolicy.from_header`` method instead.
844 .. versionadded:: 1.0
845 Support for Content Security Policy headers was added.
847 :param value: a csp header to be parsed.
848 :param on_update: an optional callable that is called every time a value
849 on the object is changed.
850 :param cls: the class for the returned object. By default
851 :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.
852 :return: a `cls` object.
853 """
854 import warnings
856 warnings.warn(
857 "The 'parse_csp_header' function is deprecated and will be removed in"
858 " Werkzeug 3.3. Use the 'Response.content_security_policy' property or"
859 " the 'ContentSecurityPolicy.from_header' method instead.",
860 DeprecationWarning,
861 stacklevel=2,
862 )
864 if cls is None:
865 cls = t.cast(type[_TAnyCSP], ds.ContentSecurityPolicy)
867 obj = cls.from_header(value)
868 obj.on_update = on_update
869 return obj
872def _parse_set_header(
873 value: str | None,
874 on_update: t.Callable[[ds.HeaderSet], None] | None = None,
875) -> ds.HeaderSet:
876 """Parse a set-like header and return a
877 :class:`~werkzeug.datastructures.HeaderSet` object:
879 >>> hs = parse_set_header('token, "quoted value"')
881 The return value is an object that treats the items case-insensitively
882 and keeps the order of the items:
884 >>> 'TOKEN' in hs
885 True
886 >>> hs.index('quoted value')
887 1
888 >>> hs
889 HeaderSet(['token', 'quoted value'])
891 To create a header from the :class:`HeaderSet` again, use the
892 :func:`dump_header` function.
894 :param value: a set header to be parsed.
895 :param on_update: an optional callable that is called every time a
896 value on the :class:`~werkzeug.datastructures.HeaderSet`
897 object is changed.
899 .. deprecated:: 3.2
900 Will be removed in Werkzeug 3.3. Use the ``HeaderSet.from_header``
901 method instead.
902 """
903 import warnings
905 warnings.warn(
906 "The 'parse_set_header' function is deprecated and will be removed in"
907 " Werkzeug 3.3. Use the 'HeaderSet.from_header' method instead.",
908 DeprecationWarning,
909 stacklevel=2,
910 )
911 obj = ds.HeaderSet.from_header(value)
912 obj._on_update = on_update
913 return obj
916def _parse_if_range_header(value: str | None) -> ds.IfRange:
917 """Parses an if-range header which can be an etag or a date. Returns
918 a :class:`~werkzeug.datastructures.IfRange` object.
920 .. deprecated:: 3.2
921 Will be removed in Werkzeug 3.3. Use the ``IfRange.from_header``
922 method instead.
924 .. versionchanged:: 2.0
925 If the value represents a datetime, it is timezone-aware.
927 .. versionadded:: 0.7
928 """
929 import warnings
931 warnings.warn(
932 "The 'parse_if_range_header' function is deprecated and will be removed in"
933 " Werkzeug 3.3. Use the 'IfRange.from_header' method instead.",
934 DeprecationWarning,
935 stacklevel=2,
936 )
937 return ds.IfRange.from_header(value)
940def _parse_range_header(
941 value: str | None, make_inclusive: bool = True
942) -> ds.Range | None:
943 """Parses a range header into a :class:`~werkzeug.datastructures.Range`
944 object. If the header is missing or malformed `None` is returned.
945 `ranges` is a list of ``(start, stop)`` tuples where the ranges are
946 non-inclusive.
948 .. deprecated:: 3.2
949 Will be removed in Werkzeug 3.3. Use the ``Range.from_header``
950 method instead.
952 .. versionadded:: 0.7
953 """
954 import warnings
956 warnings.warn(
957 "The 'parse_range_header' function is deprecated and will be removed in"
958 " Werkzeug 3.3. Use the 'Range.from_header' method instead.",
959 DeprecationWarning,
960 stacklevel=2,
961 )
962 return ds.Range.from_header(value)
965def _parse_content_range_header(
966 value: str | None,
967 on_update: t.Callable[[ds.ContentRange], None] | None = None,
968) -> ds.ContentRange | None:
969 """Parses a range header into a
970 :class:`~werkzeug.datastructures.ContentRange` object or `None` if
971 parsing is not possible.
973 .. deprecated:: 3.2
974 Will be removed in Werkzeug 3.3. Use the ``ContentRange.from_header``
975 method instead.
977 .. versionadded:: 0.7
979 :param value: a content range header to be parsed.
980 :param on_update: an optional callable that is called every time a value
981 on the :class:`~werkzeug.datastructures.ContentRange`
982 object is changed.
983 """
984 import warnings
986 warnings.warn(
987 "The 'parse_range_header' function is deprecated and will be removed in"
988 " Werkzeug 3.3. Use the 'Range.from_header' method instead.",
989 DeprecationWarning,
990 stacklevel=2,
991 )
993 if (obj := ds.ContentRange.from_header(value)) is None:
994 return None
996 obj._on_update = on_update
997 return obj
1000def quote_etag(etag: str, weak: bool = False) -> str:
1001 """Quote an etag.
1003 :param etag: the etag to quote.
1004 :param weak: set to `True` to tag it "weak".
1005 """
1006 if '"' in etag:
1007 raise ValueError("invalid etag")
1008 etag = f'"{etag}"'
1009 if weak:
1010 etag = f"W/{etag}"
1011 return etag
1014@t.overload
1015def unquote_etag(etag: str) -> tuple[str, bool]: ...
1016@t.overload
1017def unquote_etag(etag: None) -> tuple[None, None]: ...
1018def unquote_etag(
1019 etag: str | None,
1020) -> tuple[str, bool] | tuple[None, None]:
1021 """Unquote a single etag:
1023 >>> unquote_etag('W/"bar"')
1024 ('bar', True)
1025 >>> unquote_etag('"bar"')
1026 ('bar', False)
1028 :param etag: the etag identifier to unquote.
1029 :return: a ``(etag, weak)`` tuple.
1030 """
1031 if not etag:
1032 return None, None
1033 etag = etag.strip()
1034 weak = False
1035 if etag.startswith(("W/", "w/")):
1036 weak = True
1037 etag = etag[2:]
1038 if etag[:1] == etag[-1:] == '"':
1039 etag = etag[1:-1]
1040 return etag, weak
1043def _parse_etags(value: str | None) -> ds.ETags:
1044 """Parse an etag header.
1046 :param value: the tag header to parse
1047 :return: an :class:`~werkzeug.datastructures.ETags` object.
1049 .. deprecated:: 3.2
1050 Will be removed in Werkzeug 3.3. Use the 'ETags.from_header' method instead.
1051 """
1052 import warnings
1054 warnings.warn(
1055 "The 'parse_etags' function is deprecated and will be removed in"
1056 " Werkzeug 3.3. Use the 'ETags.from_header' method instead.",
1057 DeprecationWarning,
1058 stacklevel=2,
1059 )
1060 return ds.ETags.from_header(value)
1063def generate_etag(data: bytes) -> str:
1064 """Generate a strong ETag value by hashing the given data.
1066 .. versionchanged:: 3.2
1067 Use SHA3-256. SHA-1 is not allowed in FIPS-enabled systems. Use base64
1068 instead of hex digest. This increases the length from 40 to 43
1069 characters.
1071 .. versionchanged:: 2.0
1072 Use SHA-1. MD5 is not allowed in FIPS-enabled systems. This increases
1073 the length from 32 to 40 characters.
1074 """
1075 digest = hashlib.sha3_256(data, usedforsecurity=False).digest()
1076 return b64encode(digest).decode().rstrip("=")
1079def parse_date(value: str | None) -> datetime | None:
1080 """Parse an :rfc:`2822` date into a timezone-aware
1081 :class:`datetime.datetime` object, or ``None`` if parsing fails.
1083 This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It
1084 returns ``None`` if parsing fails instead of raising an exception,
1085 and always returns a timezone-aware datetime object. If the string
1086 doesn't have timezone information, it is assumed to be UTC.
1088 :param value: A string with a supported date format.
1090 .. versionchanged:: 2.0
1091 Return a timezone-aware datetime object. Use
1092 ``email.utils.parsedate_to_datetime``.
1093 """
1094 if value is None:
1095 return None
1097 try:
1098 dt = email.utils.parsedate_to_datetime(value)
1099 except (TypeError, ValueError):
1100 return None
1102 if dt.tzinfo is None:
1103 return dt.replace(tzinfo=timezone.utc)
1105 return dt
1108def http_date(
1109 timestamp: datetime | date | int | float | struct_time | None = None,
1110) -> str:
1111 """Format a datetime object or timestamp into an :rfc:`2822` date
1112 string.
1114 This is a wrapper for :func:`email.utils.format_datetime`. It
1115 assumes naive datetime objects are in UTC instead of raising an
1116 exception.
1118 :param timestamp: The datetime or timestamp to format. Defaults to
1119 the current time.
1121 .. versionchanged:: 2.0
1122 Use ``email.utils.format_datetime``. Accept ``date`` objects.
1123 """
1124 if isinstance(timestamp, date):
1125 if not isinstance(timestamp, datetime):
1126 # Assume plain date is midnight UTC.
1127 timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc)
1128 else:
1129 # Ensure datetime is timezone-aware.
1130 timestamp = _dt_as_utc(timestamp)
1132 return email.utils.format_datetime(timestamp, usegmt=True)
1134 if isinstance(timestamp, struct_time):
1135 timestamp = mktime(timestamp)
1137 return email.utils.formatdate(timestamp, usegmt=True)
1140def parse_age(value: str | None = None) -> timedelta | None:
1141 """Parses a base-10 integer count of seconds into a timedelta.
1143 If parsing fails, the return value is `None`.
1145 :param value: a string consisting of an integer represented in base-10
1146 :return: a :class:`datetime.timedelta` object or `None`.
1147 """
1148 if not value:
1149 return None
1150 try:
1151 seconds = _plain_int(value)
1152 except ValueError:
1153 return None
1154 if seconds < 0:
1155 return None
1156 try:
1157 return timedelta(seconds=seconds)
1158 except OverflowError:
1159 return None
1162def dump_age(age: timedelta | int | None = None) -> str | None:
1163 """Formats the duration as a base-10 integer.
1165 :param age: should be an integer number of seconds,
1166 a :class:`datetime.timedelta` object, or,
1167 if the age is unknown, `None` (default).
1168 """
1169 if age is None:
1170 return None
1172 if isinstance(age, timedelta):
1173 age = int(age.total_seconds())
1175 if age < 0:
1176 raise ValueError("age cannot be negative")
1178 return str(age)
1181def is_resource_modified(
1182 environ: WSGIEnvironment,
1183 etag: str | None = None,
1184 data: bytes | None = None,
1185 last_modified: datetime | str | None = None,
1186 ignore_if_range: bool = True,
1187) -> bool:
1188 """Convenience method for conditional requests.
1190 :param environ: the WSGI environment of the request to be checked.
1191 :param etag: the etag for the response for comparison.
1192 :param data: or alternatively the data of the response to automatically
1193 generate an etag using :func:`generate_etag`.
1194 :param last_modified: an optional date of the last modification.
1195 :param ignore_if_range: If `False`, `If-Range` header will be taken into
1196 account.
1197 :return: `True` if the resource was modified, otherwise `False`.
1199 .. versionchanged:: 1.0
1200 The check is run for methods other than ``GET`` and ``HEAD``.
1201 """
1202 return _sansio_http.is_resource_modified(
1203 http_range=environ.get("HTTP_RANGE"),
1204 http_if_range=environ.get("HTTP_IF_RANGE"),
1205 http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),
1206 http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),
1207 http_if_match=environ.get("HTTP_IF_MATCH"),
1208 etag=etag,
1209 data=data,
1210 last_modified=last_modified,
1211 ignore_if_range=ignore_if_range,
1212 )
1215def remove_entity_headers(
1216 headers: ds.Headers | list[tuple[str, str]],
1217 allowed: t.Iterable[str] = ("expires", "content-location"),
1218) -> None:
1219 """Remove all entity headers from a list or :class:`Headers` object. This
1220 operation works in-place. `Expires` and `Content-Location` headers are
1221 by default not removed. The reason for this is :rfc:`2616` section
1222 10.3.5 which specifies some entity headers that should be sent.
1224 .. versionchanged:: 0.5
1225 added `allowed` parameter.
1227 :param headers: a list or :class:`Headers` object.
1228 :param allowed: a list of headers that should still be allowed even though
1229 they are entity headers.
1230 """
1231 allowed = {x.lower() for x in allowed}
1232 headers[:] = [
1233 (key, value)
1234 for key, value in headers
1235 if not is_entity_header(key) or key.lower() in allowed
1236 ]
1239def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None:
1240 """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or
1241 :class:`Headers` object. This operation works in-place.
1243 .. versionadded:: 0.5
1245 :param headers: a list or :class:`Headers` object.
1246 """
1247 headers[:] = [
1248 (key, value) for key, value in headers if not is_hop_by_hop_header(key)
1249 ]
1252def is_entity_header(header: str) -> bool:
1253 """Check if a header is an entity header.
1255 .. versionadded:: 0.5
1257 :param header: the header to test.
1258 :return: `True` if it's an entity header, `False` otherwise.
1259 """
1260 return header.lower() in _entity_headers
1263def is_hop_by_hop_header(header: str) -> bool:
1264 """Check if a header is an HTTP/1.1 "Hop-by-Hop" header.
1266 .. versionadded:: 0.5
1268 :param header: the header to test.
1269 :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise.
1270 """
1271 return header.lower() in _hop_by_hop_headers
1274def parse_cookie(
1275 header: WSGIEnvironment | str | None, **kwargs: t.Any
1276) -> ds.ImmutableMultiDict[str, str]:
1277 """Parse cookies from a ``Cookie`` header as an :class:`.ImmutableMultiDict`.
1279 :param header: The ``Cookie`` header, or a WSGI environ with an
1280 ``HTTP_COOKIE`` key.
1282 .. versionchanged:: 3.2
1283 The ``cls`` parameter is deprecated and will be removed in Werkzeug 3.3.
1284 It will always be ``ImmutableMultiDict``.
1286 .. versionchanged:: 3.0
1287 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
1289 .. versionchanged:: 1.0
1290 Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.
1292 .. versionchanged:: 0.5
1293 Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls``
1294 parameter was added.
1295 """
1296 if isinstance(header, dict):
1297 cookie = header.get("HTTP_COOKIE")
1298 else:
1299 cookie = header
1301 if cookie:
1302 cookie = cookie.encode("latin1").decode()
1304 parse_kwargs: dict[str, t.Any] = {}
1306 if "cls" in kwargs:
1307 import warnings
1309 warnings.warn(
1310 "The 'cls' parameter is deprecated and will be removed in Werkzeug"
1311 " 3.3. It will always be 'ImmutableMultiDict'.",
1312 DeprecationWarning,
1313 stacklevel=2,
1314 )
1315 parse_kwargs["cls"] = kwargs["cls"]
1317 return _sansio_http.parse_cookie(cookie=cookie, **kwargs)
1320_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
1321_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)
1322_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}
1323_cookie_slash_map.update(
1324 (v.to_bytes(1, "big"), b"\\%03o" % v)
1325 for v in [*range(0x20), *b",;", *range(0x7F, 256)]
1326)
1329def dump_cookie(
1330 key: str,
1331 value: str = "",
1332 max_age: timedelta | int | None = None,
1333 expires: str | datetime | int | float | None = None,
1334 path: str | None = "/",
1335 domain: str | None = None,
1336 secure: bool = False,
1337 httponly: bool = False,
1338 sync_expires: bool = True,
1339 max_size: int = 4093,
1340 samesite: str | None = None,
1341 partitioned: bool = False,
1342) -> str:
1343 """Create a Set-Cookie header without the ``Set-Cookie`` prefix.
1345 The return value is usually restricted to ascii as the vast majority
1346 of values are properly escaped, but that is no guarantee. It's
1347 tunneled through latin1 as required by :pep:`3333`.
1349 The return value is not ASCII safe if the key contains unicode
1350 characters. This is technically against the specification but
1351 happens in the wild. It's strongly recommended to not use
1352 non-ASCII values for the keys.
1354 :param max_age: should be a number of seconds, or `None` (default) if
1355 the cookie should last only as long as the client's
1356 browser session. Additionally `timedelta` objects
1357 are accepted, too.
1358 :param expires: should be a `datetime` object or unix timestamp.
1359 :param path: limits the cookie to a given path, per default it will
1360 span the whole domain.
1361 :param domain: Use this if you want to set a cross-domain cookie. For
1362 example, ``domain="example.com"`` will set a cookie
1363 that is readable by the domain ``www.example.com``,
1364 ``foo.example.com`` etc. Otherwise, a cookie will only
1365 be readable by the domain that set it.
1366 :param secure: The cookie will only be available via HTTPS
1367 :param httponly: disallow JavaScript to access the cookie. This is an
1368 extension to the cookie standard and probably not
1369 supported by all browsers.
1370 :param charset: the encoding for string values.
1371 :param sync_expires: automatically set expires if max_age is defined
1372 but expires not.
1373 :param max_size: Warn if the final header value exceeds this size. The
1374 default, 4093, should be safely `supported by most browsers
1375 <cookie_>`_. Set to 0 to disable this check.
1376 :param samesite: Limits the scope of the cookie such that it will
1377 only be attached to requests if those requests are same-site.
1378 :param partitioned: Opts the cookie into partitioned storage. This
1379 will also set secure to True
1381 .. _`cookie`: http://browsercookielimits.squawky.net/
1383 .. versionchanged:: 3.1
1384 The ``partitioned`` parameter was added.
1386 .. versionchanged:: 3.0
1387 Passing bytes, and the ``charset`` parameter, were removed.
1389 .. versionchanged:: 2.3.3
1390 The ``path`` parameter is ``/`` by default.
1392 .. versionchanged:: 2.3.1
1393 The value allows more characters without quoting.
1395 .. versionchanged:: 2.3
1396 ``localhost`` and other names without a dot are allowed for the domain. A
1397 leading dot is ignored.
1399 .. versionchanged:: 2.3
1400 The ``path`` parameter is ``None`` by default.
1402 .. versionchanged:: 1.0.0
1403 The string ``'None'`` is accepted for ``samesite``.
1404 """
1405 if path is not None:
1406 # safe = https://url.spec.whatwg.org/#url-path-segment-string
1407 # as well as percent for things that are already quoted
1408 # excluding semicolon since it's part of the header syntax
1409 path = quote(path, safe="%!$&'()*+,/:=@")
1411 if domain:
1412 domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii")
1414 if isinstance(max_age, timedelta):
1415 max_age = int(max_age.total_seconds())
1417 if expires is not None:
1418 if not isinstance(expires, str):
1419 expires = http_date(expires)
1420 elif max_age is not None and sync_expires:
1421 expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age)
1423 if samesite is not None:
1424 samesite = samesite.title()
1426 if samesite not in {"Strict", "Lax", "None"}:
1427 raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
1429 if partitioned:
1430 secure = True
1432 # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
1433 # three octal digits, which matches http.cookies, although the RFC suggests base64.
1434 if not _cookie_no_quote_re.fullmatch(value):
1435 # Work with bytes here, since a UTF-8 character could be multiple bytes.
1436 value = _cookie_slash_re.sub(
1437 lambda m: _cookie_slash_map[m.group()], value.encode()
1438 ).decode("ascii")
1439 value = f'"{value}"'
1441 # Send a non-ASCII key as mojibake. Everything else should already be ASCII.
1442 # TODO Remove encoding dance, it seems like clients accept UTF-8 keys
1443 buf = [f"{key.encode().decode('latin1')}={value}"]
1445 for k, v in (
1446 ("Domain", domain),
1447 ("Expires", expires),
1448 ("Max-Age", max_age),
1449 ("Secure", secure),
1450 ("HttpOnly", httponly),
1451 ("Path", path),
1452 ("SameSite", samesite),
1453 ("Partitioned", partitioned),
1454 ):
1455 if v is None or v is False:
1456 continue
1458 if v is True:
1459 buf.append(k)
1460 continue
1462 buf.append(f"{k}={v}")
1464 rv = "; ".join(buf)
1466 # Warn if the final value of the cookie is larger than the limit. If the cookie is
1467 # too large, then it may be silently ignored by the browser, which can be quite hard
1468 # to debug.
1469 cookie_size = len(rv)
1471 if max_size and cookie_size > max_size:
1472 value_size = len(value)
1473 warnings.warn(
1474 f"The '{key}' cookie is too large: the value was {value_size} bytes but the"
1475 f" header required {cookie_size - value_size} extra bytes. The final size"
1476 f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may"
1477 " silently ignore cookies larger than this.",
1478 stacklevel=2,
1479 )
1481 return rv
1484def is_byte_range_valid(
1485 start: int | None, stop: int | None, length: int | None
1486) -> bool:
1487 """Checks if a given byte content range is valid for the given length.
1489 .. versionadded:: 0.7
1490 """
1491 if (start is None) != (stop is None):
1492 return False
1493 elif start is None:
1494 return length is None or length >= 0
1495 elif length is None:
1496 return 0 <= start < stop # type: ignore
1497 elif start >= stop: # type: ignore
1498 return False
1499 return 0 <= start < length
1502# circular dependencies
1503from . import datastructures as ds # noqa: E402
1504from .sansio import http as _sansio_http # noqa: E402
1506if not t.TYPE_CHECKING:
1508 def __getattr__(name: str) -> t.Any:
1509 if name == "HTTP_STATUS_CODES":
1510 import warnings
1512 warnings.warn(
1513 "The 'HTTP_STATUS_CODES' data is deprecated and will be removed in"
1514 " Werkzeug 3.3. Use Python's built-in 'http.HTTPStatus' instead.",
1515 DeprecationWarning,
1516 stacklevel=2,
1517 )
1518 return _HTTP_STATUS_CODES
1520 alts = {
1521 "dump_csp_header": "ContentSecurityPolicy.to_header",
1522 "parse_accept_header": "Accept.from_header",
1523 "parse_cache_control_header": "CacheControl.from_header",
1524 "parse_content_range_header": "ContentRange.from_header",
1525 "parse_csp_header": "ContentSecurityPolicy.from_header",
1526 "parse_etags": "ETags.from_header",
1527 "parse_if_range_header": "IfRange.from_header",
1528 "parse_range_header": "Range.from_header",
1529 "parse_set_header": "HeaderSet.from_header",
1530 }
1532 if name in alts:
1533 import warnings
1535 warnings.warn(
1536 f"The '{name}' function is deprecated and will be removed in"
1537 f" Werkzeug 3.3. Use the '{alts[name]}' method instead.",
1538 DeprecationWarning,
1539 stacklevel=2,
1540 )
1541 return globals()[f"_{name}"]
1543 raise AttributeError(name)