Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
http.py1444 linesDownload Raw Back to werkzeug
1from __future__ import annotations2 3import email.utils4import re5import typing as t6import warnings7from datetime import date8from datetime import datetime9from datetime import time10from datetime import timedelta11from datetime import timezone12from enum import Enum13from hashlib import sha114from time import mktime15from time import struct_time16from urllib.parse import quote17from urllib.parse import unquote18 19from ._internal import _dt_as_utc20from ._internal import _plain_int21 22if t.TYPE_CHECKING:23    from _typeshed.wsgi import WSGIEnvironment24 25_token_chars = frozenset(26    "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"27)28_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')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)55HTTP_STATUS_CODES = {56    100: "Continue",57    101: "Switching Protocols",58    102: "Processing",59    103: "Early Hints",  # see RFC 829760    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 584269    226: "IM Used",  # see RFC 322970    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",  # unused77    307: "Temporary Redirect",78    308: "Permanent Redirect",79    400: "Bad Request",80    401: "Unauthorized",81    402: "Payment Required",  # unused82    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 232498    421: "Misdirected Request",  # see RFC 754099    422: "Unprocessable Entity",100    423: "Locked",101    424: "Failed Dependency",102    425: "Too Early",  # see RFC 8470103    426: "Upgrade Required",104    428: "Precondition Required",  # see RFC 6585105    429: "Too Many Requests",106    431: "Request Header Fields Too Large",107    449: "Retry With",  # proprietary MS extension108    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 2295116    507: "Insufficient Storage",117    508: "Loop Detected",  # see RFC 5842118    510: "Not Extended",119    511: "Network Authentication Failed",120}121 122 123class COEP(Enum):124    """Cross Origin Embedder Policies"""125 126    UNSAFE_NONE = "unsafe-none"127    REQUIRE_CORP = "require-corp"128 129 130class COOP(Enum):131    """Cross Origin Opener Policies"""132 133    UNSAFE_NONE = "unsafe-none"134    SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"135    SAME_ORIGIN = "same-origin"136 137 138def quote_header_value(value: t.Any, allow_token: bool = True) -> str:139    """Add double quotes around a header value. If the header contains only ASCII token140    characters, it will be returned unchanged. If the header contains ``"`` or ``\\``141    characters, they will be escaped with an additional ``\\`` character.142 143    This is the reverse of :func:`unquote_header_value`.144 145    :param value: The value to quote. Will be converted to a string.146    :param allow_token: Disable to quote the value even if it only has token characters.147 148    .. versionchanged:: 3.0149        Passing bytes is not supported.150 151    .. versionchanged:: 3.0152        The ``extra_chars`` parameter is removed.153 154    .. versionchanged:: 2.3155        The value is quoted if it is the empty string.156 157    .. versionadded:: 0.5158    """159    value_str = str(value)160 161    if not value_str:162        return '""'163 164    if allow_token:165        token_chars = _token_chars166 167        if token_chars.issuperset(value_str):168            return value_str169 170    value_str = value_str.replace("\\", "\\\\").replace('"', '\\"')171    return f'"{value_str}"'172 173 174_unslash_re = re.compile(r"\\(.)", re.A)175 176 177def unquote_header_value(value: str) -> str:178    """Remove double quotes and backslash escapes from a header value.179 180    This is the reverse of :func:`quote_header_value`.181 182    :param value: The header value to unquote.183 184    .. versionchanged:: 3.2185        Removes escape preceding any character.186 187    .. versionchanged:: 3.0188        The ``is_filename`` parameter is removed.189    """190    if len(value) >= 2 and value[0] == value[-1] == '"':191        return _unslash_re.sub(r"\g<1>", value[1:-1])192 193    return value194 195 196def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:197    """Produce a header value and ``key=value`` parameters separated by semicolons198    ``;``. For example, the ``Content-Type`` header.199 200    .. code-block:: python201 202        dump_options_header("text/html", {"charset": "UTF-8"})203        'text/html; charset=UTF-8'204 205    This is the reverse of :func:`parse_options_header`.206 207    If a value contains non-token characters, it will be quoted.208 209    If a value is ``None``, the parameter is skipped.210 211    In some keys for some headers, a UTF-8 value can be encoded using a special212    ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will213    not produce that format automatically, but if a given key ends with an asterisk214    ``*``, the value is assumed to have that form and will not be quoted further.215 216    :param header: The primary header value.217    :param options: Parameters to encode as ``key=value`` pairs.218 219    .. versionchanged:: 2.3220        Keys with ``None`` values are skipped rather than treated as a bare key.221 222    .. versionchanged:: 2.2.3223        If a key ends with ``*``, its value will not be quoted.224    """225    segments = []226 227    if header is not None:228        segments.append(header)229 230    for key, value in options.items():231        if value is None:232            continue233 234        if key[-1] == "*":235            segments.append(f"{key}={value}")236        else:237            segments.append(f"{key}={quote_header_value(value)}")238 239    return "; ".join(segments)240 241 242def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str:243    """Produce a header value from a list of items or ``key=value`` pairs, separated by244    commas ``,``.245 246    This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and247    :func:`parse_set_header`.248 249    If a value contains non-token characters, it will be quoted.250 251    If a value is ``None``, the key is output alone.252 253    In some keys for some headers, a UTF-8 value can be encoded using a special254    ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will255    not produce that format automatically, but if a given key ends with an asterisk256    ``*``, the value is assumed to have that form and will not be quoted further.257 258    .. code-block:: python259 260        dump_header(["foo", "bar baz"])261        'foo, "bar baz"'262 263        dump_header({"foo": "bar baz"})264        'foo="bar baz"'265 266    :param iterable: The items to create a header from.267 268    .. versionchanged:: 3.0269        The ``allow_token`` parameter is removed.270 271    .. versionchanged:: 2.2.3272        If a key ends with ``*``, its value will not be quoted.273    """274    if isinstance(iterable, dict):275        items = []276 277        for key, value in iterable.items():278            if value is None:279                items.append(key)280            elif key[-1] == "*":281                items.append(f"{key}={value}")282            else:283                items.append(f"{key}={quote_header_value(value)}")284    else:285        items = [quote_header_value(x) for x in iterable]286 287    return ", ".join(items)288 289 290def dump_csp_header(header: ds.ContentSecurityPolicy) -> str:291    """Dump a Content Security Policy header.292 293    These are structured into policies such as "default-src 'self';294    script-src 'self'".295 296    .. versionadded:: 1.0.0297       Support for Content Security Policy headers was added.298 299    """300    return "; ".join(f"{key} {value}" for key, value in header.items())301 302 303def parse_list_header(value: str) -> list[str]:304    """Parse a header value that consists of a list of comma separated items according305    to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.306 307    Surrounding quotes are removed from items, but internal quotes are left for308    future parsing. Empty values are discarded.309 310    .. code-block:: python311 312        parse_list_header('token, "quoted value"')313        ['token', 'quoted value']314 315    This is the reverse of :func:`dump_header`.316 317    :param value: The header value to parse.318 319    .. versionchanged:: 3.2320        Quotes and escapes are kept if only part of an item is quoted. Empty321        values are omitted. An empty list is returned if the value contains an322        unclosed quoted string.323    """324    items = []325    item = ""326    escape = False327    quote = False328 329    for char in value:330        if escape:331            escape = False332            item += char333            continue334 335        if quote:336            if char == "\\":337                escape = True338            elif char == '"':339                quote = False340 341            item += char342            continue343 344        if char == ",":345            items.append(item)346            item = ""347            continue348 349        if char == '"':350            quote = True351 352        item += char353 354    if quote:355        # invalid, unclosed quoted string356        return []357 358    items.append(item)359    return [360        unquote_header_value(item) for item in (item.strip() for item in items) if item361    ]362 363 364def parse_dict_header(value: str) -> dict[str, str | None]:365    """Parse a list header using :func:`parse_list_header`, then parse each item as a366    ``key=value`` pair.367 368    .. code-block:: python369 370        parse_dict_header('a=b, c="d, e", f')371        {"a": "b", "c": "d, e", "f": None}372 373    This is the reverse of :func:`dump_header`.374 375    If a key does not have a value, it is ``None``.376 377    This handles charsets for values as described in378    `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,379    and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.380 381    :param value: The header value to parse.382 383    .. versionchanged:: 3.2384        An empty dict is returned if the value contains an unclosed quoted385        string.386 387    .. versionchanged:: 3.0388        Passing bytes is not supported.389 390    .. versionchanged:: 3.0391        The ``cls`` argument is removed.392 393    .. versionchanged:: 2.3394        Added support for ``key*=charset''value`` encoded items.395 396    .. versionchanged:: 0.9397       The ``cls`` argument was added.398    """399    result: dict[str, str | None] = {}400 401    for item in parse_list_header(value):402        key, has_value, value = item.partition("=")403        key = key.strip()404 405        if not key:406            # =value is not valid407            continue408 409        if not has_value:410            result[key] = None411            continue412 413        value = value.strip()414        encoding: str | None = None415 416        if key[-1] == "*":417            # key*=charset''value becomes key=value, where value is percent encoded418            # adapted from parse_options_header, without the continuation handling419            key = key[:-1]420            match = _charset_value_re.match(value)421 422            if match:423                # If there is a charset marker in the value, split it off.424                encoding, value = match.groups()425                encoding = encoding.lower()426 427            # A safe list of encodings. Modern clients should only send ASCII or UTF-8.428            # This list will not be extended further. An invalid encoding will leave the429            # value quoted.430            if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:431                # invalid bytes are replaced during unquoting432                value = unquote(value, encoding=encoding)433 434        result[key] = unquote_header_value(value)435 436    return result437 438 439# https://httpwg.org/specs/rfc9110.html#parameter440_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII)441_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII)442# https://www.rfc-editor.org/rfc/rfc2231#section-4443_charset_value_re = re.compile(444    r"""445    ([\w!#$%&*+\-.^`|~]*)'  # charset part, could be empty446    [\w!#$%&*+\-.^`|~]*'  # don't care about language part, usually empty447    ([\w!#$%&'*+\-.^`|~]+)  # one or more token chars with percent encoding448    """,449    re.ASCII | re.VERBOSE,450)451# https://www.rfc-editor.org/rfc/rfc2231#section-3452_continuation_re = re.compile(r"\*(\d+)$", re.ASCII)453 454 455def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:456    """Parse a header that consists of a value with ``key=value`` parameters separated457    by semicolons ``;``. For example, the ``Content-Type`` header.458 459    .. code-block:: python460 461        parse_options_header("text/html; charset=UTF-8")462        ('text/html', {'charset': 'UTF-8'})463 464        parse_options_header("")465        ("", {})466 467    This is the reverse of :func:`dump_options_header`.468 469    This parses valid parameter parts as described in470    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are471    skipped.472 473    This handles continuations and charsets as described in474    `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as475    strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,476    otherwise the value remains quoted.477 478    Clients may not be consistent in how they handle a quote character within a quoted479    value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__480    replaces it with ``%22`` in multipart form data.481    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash482    escapes in HTTP headers. Both are decoded to the ``"`` character.483 484    Clients may not be consistent in how they handle non-ASCII characters. HTML485    documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with486    HTML character references, which can be decoded using :func:`html.unescape`.487 488    :param value: The header value to parse.489    :return: ``(value, options)``, where ``options`` is a dict490 491    .. versionchanged:: 2.3492        Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted493        values, are discarded instead of treating as ``None``.494 495    .. versionchanged:: 2.3496        Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.497 498    .. versionchanged:: 2.3499        Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.500 501    .. versionchanged:: 2.2502        Option names are always converted to lowercase.503 504    .. versionchanged:: 2.2505        The ``multiple`` parameter was removed.506 507    .. versionchanged:: 0.15508        :rfc:`2231` parameter continuations are handled.509 510    .. versionadded:: 0.5511    """512    if value is None:513        return "", {}514 515    value, _, rest = value.partition(";")516    value = value.strip()517    rest = rest.strip()518 519    if not value or not rest:520        # empty (invalid) value, or value without options521        return value, {}522 523    # Collect all valid key=value parts without processing the value.524    parts: list[tuple[str, str]] = []525 526    while True:527        if (m := _parameter_key_re.match(rest)) is not None:528            pk = m.group(1).lower()529            rest = rest[m.end() :]530 531            # Value may be a token.532            if (m := _parameter_token_value_re.match(rest)) is not None:533                parts.append((pk, m.group()))534 535            # Value may be a quoted string, find the closing quote.536            elif rest[:1] == '"':537                pos = 1538                length = len(rest)539 540                while pos < length:541                    if rest[pos : pos + 2] in {"\\\\", '\\"'}:542                        # Consume escaped slashes and quotes.543                        pos += 2544                    elif rest[pos] == '"':545                        # Stop at an unescaped quote.546                        parts.append((pk, rest[: pos + 1]))547                        rest = rest[pos + 1 :]548                        break549                    else:550                        # Consume any other character.551                        pos += 1552 553        # Find the next section delimited by `;`, if any.554        if (end := rest.find(";")) == -1:555            break556 557        rest = rest[end + 1 :].lstrip()558 559    options: dict[str, str] = {}560    encoding: str | None = None561    continued_encoding: str | None = None562 563    # For each collected part, process optional charset and continuation,564    # unquote quoted values.565    for pk, pv in parts:566        if pk[-1] == "*":567            # key*=charset''value becomes key=value, where value is percent encoded568            pk = pk[:-1]569            match = _charset_value_re.match(pv)570 571            if match:572                # If there is a valid charset marker in the value, split it off.573                encoding, pv = match.groups()574                # This might be the empty string, handled next.575                encoding = encoding.lower()576 577            # No charset marker, or marker with empty charset value.578            if not encoding:579                encoding = continued_encoding580 581            # A safe list of encodings. Modern clients should only send ASCII or UTF-8.582            # This list will not be extended further. An invalid encoding will leave the583            # value quoted.584            if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:585                # Continuation parts don't require their own charset marker. This is586                # looser than the RFC, it will persist across different keys and allows587                # changing the charset during a continuation. But this implementation is588                # much simpler than tracking the full state.589                continued_encoding = encoding590                # invalid bytes are replaced during unquoting591                pv = unquote(pv, encoding=encoding)592 593        # Remove quotes. At this point the value cannot be empty or a single quote.594        if pv[0] == pv[-1] == '"':595            # HTTP headers use slash, multipart form data uses percent596            pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"')597 598        match = _continuation_re.search(pk)599 600        if match:601            # key*0=a; key*1=b becomes key=ab602            pk = pk[: match.start()]603            options[pk] = options.get(pk, "") + pv604        else:605            options[pk] = pv606 607    return value, options608 609 610_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII)611_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")612 613 614@t.overload615def parse_accept_header(value: str | None) -> ds.Accept: ...616 617 618@t.overload619def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ...620 621 622def parse_accept_header(623    value: str | None, cls: type[_TAnyAccept] | None = None624) -> _TAnyAccept:625    """Parse an ``Accept`` header according to626    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__.627 628    Returns an :class:`.Accept` instance, which can sort and inspect items based on629    their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or630    ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.631 632    :param value: The header value to parse.633    :param cls: The :class:`.Accept` class to wrap the result in.634    :return: An instance of ``cls``.635 636    .. versionchanged:: 2.3637        Parse according to RFC 9110. Items with invalid ``q`` values are skipped.638    """639    if cls is None:640        cls = t.cast(type[_TAnyAccept], ds.Accept)641 642    if not value:643        return cls(None)644 645    result = []646 647    for item in parse_list_header(value):648        item, options = parse_options_header(item)649 650        if "q" in options:651            # pop q, remaining options are reconstructed652            q_str = options.pop("q").strip()653 654            if _q_value_re.fullmatch(q_str) is None:655                # ignore an invalid q656                continue657 658            q = float(q_str)659 660            if q < 0 or q > 1:661                # ignore an invalid q662                continue663        else:664            q = 1665 666        if options:667            # reconstruct the media type with any options668            item = dump_options_header(item, options)669 670        result.append((item, q))671 672    return cls(result)673 674 675_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")676 677 678@t.overload679def parse_cache_control_header(680    value: str | None,681    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,682) -> ds.RequestCacheControl: ...683 684 685@t.overload686def parse_cache_control_header(687    value: str | None,688    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,689    cls: type[_TAnyCC] = ...,690) -> _TAnyCC: ...691 692 693def parse_cache_control_header(694    value: str | None,695    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,696    cls: type[_TAnyCC] | None = None,697) -> _TAnyCC:698    """Parse a cache control header.  The RFC differs between response and699    request cache control, this method does not.  It's your responsibility700    to not use the wrong control statements.701 702    .. versionadded:: 0.5703       The `cls` was added.  If not specified an immutable704       :class:`~werkzeug.datastructures.RequestCacheControl` is returned.705 706    :param value: a cache control header to be parsed.707    :param on_update: an optional callable that is called every time a value708                      on the :class:`~werkzeug.datastructures.CacheControl`709                      object is changed.710    :param cls: the class for the returned object.  By default711                :class:`~werkzeug.datastructures.RequestCacheControl` is used.712    :return: a `cls` object.713    """714    if cls is None:715        cls = t.cast("type[_TAnyCC]", ds.RequestCacheControl)716 717    if not value:718        return cls((), on_update)719 720    return cls(parse_dict_header(value), on_update)721 722 723_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")724 725 726@t.overload727def parse_csp_header(728    value: str | None,729    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,730) -> ds.ContentSecurityPolicy: ...731 732 733@t.overload734def parse_csp_header(735    value: str | None,736    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,737    cls: type[_TAnyCSP] = ...,738) -> _TAnyCSP: ...739 740 741def parse_csp_header(742    value: str | None,743    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,744    cls: type[_TAnyCSP] | None = None,745) -> _TAnyCSP:746    """Parse a Content Security Policy header.747 748    .. versionadded:: 1.0.0749       Support for Content Security Policy headers was added.750 751    :param value: a csp header to be parsed.752    :param on_update: an optional callable that is called every time a value753                      on the object is changed.754    :param cls: the class for the returned object.  By default755                :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.756    :return: a `cls` object.757    """758    if cls is None:759        cls = t.cast("type[_TAnyCSP]", ds.ContentSecurityPolicy)760 761    if value is None:762        return cls((), on_update)763 764    items = []765 766    for policy in value.split(";"):767        policy = policy.strip()768 769        # Ignore badly formatted policies (no space)770        if " " in policy:771            directive, value = policy.strip().split(" ", 1)772            items.append((directive.strip(), value.strip()))773 774    return cls(items, on_update)775 776 777def parse_set_header(778    value: str | None,779    on_update: t.Callable[[ds.HeaderSet], None] | None = None,780) -> ds.HeaderSet:781    """Parse a set-like header and return a782    :class:`~werkzeug.datastructures.HeaderSet` object:783 784    >>> hs = parse_set_header('token, "quoted value"')785 786    The return value is an object that treats the items case-insensitively787    and keeps the order of the items:788 789    >>> 'TOKEN' in hs790    True791    >>> hs.index('quoted value')792    1793    >>> hs794    HeaderSet(['token', 'quoted value'])795 796    To create a header from the :class:`HeaderSet` again, use the797    :func:`dump_header` function.798 799    :param value: a set header to be parsed.800    :param on_update: an optional callable that is called every time a801                      value on the :class:`~werkzeug.datastructures.HeaderSet`802                      object is changed.803    :return: a :class:`~werkzeug.datastructures.HeaderSet`804    """805    if not value:806        return ds.HeaderSet(None, on_update)807    return ds.HeaderSet(parse_list_header(value), on_update)808 809 810def parse_if_range_header(value: str | None) -> ds.IfRange:811    """Parses an if-range header which can be an etag or a date.  Returns812    a :class:`~werkzeug.datastructures.IfRange` object.813 814    .. versionchanged:: 2.0815        If the value represents a datetime, it is timezone-aware.816 817    .. versionadded:: 0.7818    """819    if not value:820        return ds.IfRange()821    date = parse_date(value)822    if date is not None:823        return ds.IfRange(date=date)824    # drop weakness information825    return ds.IfRange(unquote_etag(value)[0])826 827 828def parse_range_header(829    value: str | None, make_inclusive: bool = True830) -> ds.Range | None:831    """Parses a range header into a :class:`~werkzeug.datastructures.Range`832    object.  If the header is missing or malformed `None` is returned.833    `ranges` is a list of ``(start, stop)`` tuples where the ranges are834    non-inclusive.835 836    .. versionadded:: 0.7837    """838    if not value or "=" not in value:839        return None840 841    ranges = []842    last_end = 0843    units, rng = value.split("=", 1)844    units = units.strip().lower()845 846    for item in rng.split(","):847        item = item.strip()848        if "-" not in item:849            return None850        if item.startswith("-"):851            if last_end < 0:852                return None853            try:854                begin = _plain_int(item)855            except ValueError:856                return None857            end = None858            last_end = -1859        elif "-" in item:860            begin_str, end_str = item.split("-", 1)861            begin_str = begin_str.strip()862            end_str = end_str.strip()863 864            try:865                begin = _plain_int(begin_str)866            except ValueError:867                return None868 869            if begin < last_end or last_end < 0:870                return None871            if end_str:872                try:873                    end = _plain_int(end_str) + 1874                except ValueError:875                    return None876 877                if begin >= end:878                    return None879            else:880                end = None881            last_end = end if end is not None else -1882        ranges.append((begin, end))883 884    return ds.Range(units, ranges)885 886 887def parse_content_range_header(888    value: str | None,889    on_update: t.Callable[[ds.ContentRange], None] | None = None,890) -> ds.ContentRange | None:891    """Parses a range header into a892    :class:`~werkzeug.datastructures.ContentRange` object or `None` if893    parsing is not possible.894 895    .. versionadded:: 0.7896 897    :param value: a content range header to be parsed.898    :param on_update: an optional callable that is called every time a value899                      on the :class:`~werkzeug.datastructures.ContentRange`900                      object is changed.901    """902    if value is None:903        return None904    try:905        units, rangedef = (value or "").strip().split(None, 1)906    except ValueError:907        return None908 909    if "/" not in rangedef:910        return None911    rng, length_str = rangedef.split("/", 1)912    if length_str == "*":913        length = None914    else:915        try:916            length = _plain_int(length_str)917        except ValueError:918            return None919 920    if rng == "*":921        if not is_byte_range_valid(None, None, length):922            return None923 924        return ds.ContentRange(units, None, None, length, on_update=on_update)925    elif "-" not in rng:926        return None927 928    start_str, stop_str = rng.split("-", 1)929    try:930        start = _plain_int(start_str)931        stop = _plain_int(stop_str) + 1932    except ValueError:933        return None934 935    if is_byte_range_valid(start, stop, length):936        return ds.ContentRange(units, start, stop, length, on_update=on_update)937 938    return None939 940 941def quote_etag(etag: str, weak: bool = False) -> str:942    """Quote an etag.943 944    :param etag: the etag to quote.945    :param weak: set to `True` to tag it "weak".946    """947    if '"' in etag:948        raise ValueError("invalid etag")949    etag = f'"{etag}"'950    if weak:951        etag = f"W/{etag}"952    return etag953 954 955@t.overload956def unquote_etag(etag: str) -> tuple[str, bool]: ...957@t.overload958def unquote_etag(etag: None) -> tuple[None, None]: ...959def unquote_etag(960    etag: str | None,961) -> tuple[str, bool] | tuple[None, None]:962    """Unquote a single etag:963 964    >>> unquote_etag('W/"bar"')965    ('bar', True)966    >>> unquote_etag('"bar"')967    ('bar', False)968 969    :param etag: the etag identifier to unquote.970    :return: a ``(etag, weak)`` tuple.971    """972    if not etag:973        return None, None974    etag = etag.strip()975    weak = False976    if etag.startswith(("W/", "w/")):977        weak = True978        etag = etag[2:]979    if etag[:1] == etag[-1:] == '"':980        etag = etag[1:-1]981    return etag, weak982 983 984def parse_etags(value: str | None) -> ds.ETags:985    """Parse an etag header.986 987    :param value: the tag header to parse988    :return: an :class:`~werkzeug.datastructures.ETags` object.989    """990    if not value:991        return ds.ETags()992    strong = []993    weak = []994    end = len(value)995    pos = 0996    while pos < end:997        match = _etag_re.match(value, pos)998        if match is None:999            break1000        is_weak, quoted, raw = match.groups()1001        if raw == "*":1002            return ds.ETags(star_tag=True)1003        elif quoted:1004            raw = quoted1005        if is_weak:1006            weak.append(raw)1007        else:1008            strong.append(raw)1009        pos = match.end()1010    return ds.ETags(strong, weak)1011 1012 1013def generate_etag(data: bytes) -> str:1014    """Generate an etag for some data.1015 1016    .. versionchanged:: 2.01017        Use SHA-1. MD5 may not be available in some environments.1018    """1019    return sha1(data).hexdigest()1020 1021 1022def parse_date(value: str | None) -> datetime | None:1023    """Parse an :rfc:`2822` date into a timezone-aware1024    :class:`datetime.datetime` object, or ``None`` if parsing fails.1025 1026    This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It1027    returns ``None`` if parsing fails instead of raising an exception,1028    and always returns a timezone-aware datetime object. If the string1029    doesn't have timezone information, it is assumed to be UTC.1030 1031    :param value: A string with a supported date format.1032 1033    .. versionchanged:: 2.01034        Return a timezone-aware datetime object. Use1035        ``email.utils.parsedate_to_datetime``.1036    """1037    if value is None:1038        return None1039 1040    try:1041        dt = email.utils.parsedate_to_datetime(value)1042    except (TypeError, ValueError):1043        return None1044 1045    if dt.tzinfo is None:1046        return dt.replace(tzinfo=timezone.utc)1047 1048    return dt1049 1050 1051def http_date(1052    timestamp: datetime | date | int | float | struct_time | None = None,1053) -> str:1054    """Format a datetime object or timestamp into an :rfc:`2822` date1055    string.1056 1057    This is a wrapper for :func:`email.utils.format_datetime`. It1058    assumes naive datetime objects are in UTC instead of raising an1059    exception.1060 1061    :param timestamp: The datetime or timestamp to format. Defaults to1062        the current time.1063 1064    .. versionchanged:: 2.01065        Use ``email.utils.format_datetime``. Accept ``date`` objects.1066    """1067    if isinstance(timestamp, date):1068        if not isinstance(timestamp, datetime):1069            # Assume plain date is midnight UTC.1070            timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc)1071        else:1072            # Ensure datetime is timezone-aware.1073            timestamp = _dt_as_utc(timestamp)1074 1075        return email.utils.format_datetime(timestamp, usegmt=True)1076 1077    if isinstance(timestamp, struct_time):1078        timestamp = mktime(timestamp)1079 1080    return email.utils.formatdate(timestamp, usegmt=True)1081 1082 1083def parse_age(value: str | None = None) -> timedelta | None:1084    """Parses a base-10 integer count of seconds into a timedelta.1085 1086    If parsing fails, the return value is `None`.1087 1088    :param value: a string consisting of an integer represented in base-101089    :return: a :class:`datetime.timedelta` object or `None`.1090    """1091    if not value:1092        return None1093    try:1094        seconds = int(value)1095    except ValueError:1096        return None1097    if seconds < 0:1098        return None1099    try:1100        return timedelta(seconds=seconds)1101    except OverflowError:1102        return None1103 1104 1105def dump_age(age: timedelta | int | None = None) -> str | None:1106    """Formats the duration as a base-10 integer.1107 1108    :param age: should be an integer number of seconds,1109                a :class:`datetime.timedelta` object, or,1110                if the age is unknown, `None` (default).1111    """1112    if age is None:1113        return None1114    if isinstance(age, timedelta):1115        age = int(age.total_seconds())1116    else:1117        age = int(age)1118 1119    if age < 0:1120        raise ValueError("age cannot be negative")1121 1122    return str(age)1123 1124 1125def is_resource_modified(1126    environ: WSGIEnvironment,1127    etag: str | None = None,1128    data: bytes | None = None,1129    last_modified: datetime | str | None = None,1130    ignore_if_range: bool = True,1131) -> bool:1132    """Convenience method for conditional requests.1133 1134    :param environ: the WSGI environment of the request to be checked.1135    :param etag: the etag for the response for comparison.1136    :param data: or alternatively the data of the response to automatically1137                 generate an etag using :func:`generate_etag`.1138    :param last_modified: an optional date of the last modification.1139    :param ignore_if_range: If `False`, `If-Range` header will be taken into1140                            account.1141    :return: `True` if the resource was modified, otherwise `False`.1142 1143    .. versionchanged:: 2.01144        SHA-1 is used to generate an etag value for the data. MD5 may1145        not be available in some environments.1146 1147    .. versionchanged:: 1.0.01148        The check is run for methods other than ``GET`` and ``HEAD``.1149    """1150    return _sansio_http.is_resource_modified(1151        http_range=environ.get("HTTP_RANGE"),1152        http_if_range=environ.get("HTTP_IF_RANGE"),1153        http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),1154        http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),1155        http_if_match=environ.get("HTTP_IF_MATCH"),1156        etag=etag,1157        data=data,1158        last_modified=last_modified,1159        ignore_if_range=ignore_if_range,1160    )1161 1162 1163def remove_entity_headers(1164    headers: ds.Headers | list[tuple[str, str]],1165    allowed: t.Iterable[str] = ("expires", "content-location"),1166) -> None:1167    """Remove all entity headers from a list or :class:`Headers` object.  This1168    operation works in-place.  `Expires` and `Content-Location` headers are1169    by default not removed.  The reason for this is :rfc:`2616` section1170    10.3.5 which specifies some entity headers that should be sent.1171 1172    .. versionchanged:: 0.51173       added `allowed` parameter.1174 1175    :param headers: a list or :class:`Headers` object.1176    :param allowed: a list of headers that should still be allowed even though1177                    they are entity headers.1178    """1179    allowed = {x.lower() for x in allowed}1180    headers[:] = [1181        (key, value)1182        for key, value in headers1183        if not is_entity_header(key) or key.lower() in allowed1184    ]1185 1186 1187def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None:1188    """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or1189    :class:`Headers` object.  This operation works in-place.1190 1191    .. versionadded:: 0.51192 1193    :param headers: a list or :class:`Headers` object.1194    """1195    headers[:] = [1196        (key, value) for key, value in headers if not is_hop_by_hop_header(key)1197    ]1198 1199 1200def is_entity_header(header: str) -> bool:

Showing the first 1,200 of 1444 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai