Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
http.py1371 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 unquote18from urllib.request import parse_http_list as _parse_list_header19 20from ._internal import _dt_as_utc21from ._internal import _plain_int22 23if t.TYPE_CHECKING:24    from _typeshed.wsgi import WSGIEnvironment25 26_token_chars = frozenset(27    "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"28)29_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')30_entity_headers = frozenset(31    [32        "allow",33        "content-encoding",34        "content-language",35        "content-length",36        "content-location",37        "content-md5",38        "content-range",39        "content-type",40        "expires",41        "last-modified",42    ]43)44_hop_by_hop_headers = frozenset(45    [46        "connection",47        "keep-alive",48        "proxy-authenticate",49        "proxy-authorization",50        "te",51        "trailer",52        "transfer-encoding",53        "upgrade",54    ]55)56HTTP_STATUS_CODES = {57    100: "Continue",58    101: "Switching Protocols",59    102: "Processing",60    103: "Early Hints",  # see RFC 829761    200: "OK",62    201: "Created",63    202: "Accepted",64    203: "Non Authoritative Information",65    204: "No Content",66    205: "Reset Content",67    206: "Partial Content",68    207: "Multi Status",69    208: "Already Reported",  # see RFC 584270    226: "IM Used",  # see RFC 322971    300: "Multiple Choices",72    301: "Moved Permanently",73    302: "Found",74    303: "See Other",75    304: "Not Modified",76    305: "Use Proxy",77    306: "Switch Proxy",  # unused78    307: "Temporary Redirect",79    308: "Permanent Redirect",80    400: "Bad Request",81    401: "Unauthorized",82    402: "Payment Required",  # unused83    403: "Forbidden",84    404: "Not Found",85    405: "Method Not Allowed",86    406: "Not Acceptable",87    407: "Proxy Authentication Required",88    408: "Request Timeout",89    409: "Conflict",90    410: "Gone",91    411: "Length Required",92    412: "Precondition Failed",93    413: "Request Entity Too Large",94    414: "Request URI Too Long",95    415: "Unsupported Media Type",96    416: "Requested Range Not Satisfiable",97    417: "Expectation Failed",98    418: "I'm a teapot",  # see RFC 232499    421: "Misdirected Request",  # see RFC 7540100    422: "Unprocessable Entity",101    423: "Locked",102    424: "Failed Dependency",103    425: "Too Early",  # see RFC 8470104    426: "Upgrade Required",105    428: "Precondition Required",  # see RFC 6585106    429: "Too Many Requests",107    431: "Request Header Fields Too Large",108    449: "Retry With",  # proprietary MS extension109    451: "Unavailable For Legal Reasons",110    500: "Internal Server Error",111    501: "Not Implemented",112    502: "Bad Gateway",113    503: "Service Unavailable",114    504: "Gateway Timeout",115    505: "HTTP Version Not Supported",116    506: "Variant Also Negotiates",  # see RFC 2295117    507: "Insufficient Storage",118    508: "Loop Detected",  # see RFC 5842119    510: "Not Extended",120    511: "Network Authentication Failed",121}122 123 124class COEP(Enum):125    """Cross Origin Embedder Policies"""126 127    UNSAFE_NONE = "unsafe-none"128    REQUIRE_CORP = "require-corp"129 130 131class COOP(Enum):132    """Cross Origin Opener Policies"""133 134    UNSAFE_NONE = "unsafe-none"135    SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"136    SAME_ORIGIN = "same-origin"137 138 139def quote_header_value(value: t.Any, allow_token: bool = True) -> str:140    """Add double quotes around a header value. If the header contains only ASCII token141    characters, it will be returned unchanged. If the header contains ``"`` or ``\\``142    characters, they will be escaped with an additional ``\\`` character.143 144    This is the reverse of :func:`unquote_header_value`.145 146    :param value: The value to quote. Will be converted to a string.147    :param allow_token: Disable to quote the value even if it only has token characters.148 149    .. versionchanged:: 3.0150        Passing bytes is not supported.151 152    .. versionchanged:: 3.0153        The ``extra_chars`` parameter is removed.154 155    .. versionchanged:: 2.3156        The value is quoted if it is the empty string.157 158    .. versionadded:: 0.5159    """160    value_str = str(value)161 162    if not value_str:163        return '""'164 165    if allow_token:166        token_chars = _token_chars167 168        if token_chars.issuperset(value_str):169            return value_str170 171    value_str = value_str.replace("\\", "\\\\").replace('"', '\\"')172    return f'"{value_str}"'173 174 175def unquote_header_value(value: str) -> str:176    """Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a177    header value.178 179    This is the reverse of :func:`quote_header_value`.180 181    :param value: The header value to unquote.182 183    .. versionchanged:: 3.0184        The ``is_filename`` parameter is removed.185    """186    if len(value) >= 2 and value[0] == value[-1] == '"':187        value = value[1:-1]188        return value.replace("\\\\", "\\").replace('\\"', '"')189 190    return value191 192 193def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:194    """Produce a header value and ``key=value`` parameters separated by semicolons195    ``;``. For example, the ``Content-Type`` header.196 197    .. code-block:: python198 199        dump_options_header("text/html", {"charset": "UTF-8"})200        'text/html; charset=UTF-8'201 202    This is the reverse of :func:`parse_options_header`.203 204    If a value contains non-token characters, it will be quoted.205 206    If a value is ``None``, the parameter is skipped.207 208    In some keys for some headers, a UTF-8 value can be encoded using a special209    ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will210    not produce that format automatically, but if a given key ends with an asterisk211    ``*``, the value is assumed to have that form and will not be quoted further.212 213    :param header: The primary header value.214    :param options: Parameters to encode as ``key=value`` pairs.215 216    .. versionchanged:: 2.3217        Keys with ``None`` values are skipped rather than treated as a bare key.218 219    .. versionchanged:: 2.2.3220        If a key ends with ``*``, its value will not be quoted.221    """222    segments = []223 224    if header is not None:225        segments.append(header)226 227    for key, value in options.items():228        if value is None:229            continue230 231        if key[-1] == "*":232            segments.append(f"{key}={value}")233        else:234            segments.append(f"{key}={quote_header_value(value)}")235 236    return "; ".join(segments)237 238 239def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str:240    """Produce a header value from a list of items or ``key=value`` pairs, separated by241    commas ``,``.242 243    This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and244    :func:`parse_set_header`.245 246    If a value contains non-token characters, it will be quoted.247 248    If a value is ``None``, the key is output alone.249 250    In some keys for some headers, a UTF-8 value can be encoded using a special251    ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will252    not produce that format automatically, but if a given key ends with an asterisk253    ``*``, the value is assumed to have that form and will not be quoted further.254 255    .. code-block:: python256 257        dump_header(["foo", "bar baz"])258        'foo, "bar baz"'259 260        dump_header({"foo": "bar baz"})261        'foo="bar baz"'262 263    :param iterable: The items to create a header from.264 265    .. versionchanged:: 3.0266        The ``allow_token`` parameter is removed.267 268    .. versionchanged:: 2.2.3269        If a key ends with ``*``, its value will not be quoted.270    """271    if isinstance(iterable, dict):272        items = []273 274        for key, value in iterable.items():275            if value is None:276                items.append(key)277            elif key[-1] == "*":278                items.append(f"{key}={value}")279            else:280                items.append(f"{key}={quote_header_value(value)}")281    else:282        items = [quote_header_value(x) for x in iterable]283 284    return ", ".join(items)285 286 287def dump_csp_header(header: ds.ContentSecurityPolicy) -> str:288    """Dump a Content Security Policy header.289 290    These are structured into policies such as "default-src 'self';291    script-src 'self'".292 293    .. versionadded:: 1.0.0294       Support for Content Security Policy headers was added.295 296    """297    return "; ".join(f"{key} {value}" for key, value in header.items())298 299 300def parse_list_header(value: str) -> list[str]:301    """Parse a header value that consists of a list of comma separated items according302    to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.303 304    This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes305    from values.306 307    .. code-block:: python308 309        parse_list_header('token, "quoted value"')310        ['token', 'quoted value']311 312    This is the reverse of :func:`dump_header`.313 314    :param value: The header value to parse.315    """316    result = []317 318    for item in _parse_list_header(value):319        if len(item) >= 2 and item[0] == item[-1] == '"':320            item = item[1:-1]321 322        result.append(item)323 324    return result325 326 327def parse_dict_header(value: str) -> dict[str, str | None]:328    """Parse a list header using :func:`parse_list_header`, then parse each item as a329    ``key=value`` pair.330 331    .. code-block:: python332 333        parse_dict_header('a=b, c="d, e", f')334        {"a": "b", "c": "d, e", "f": None}335 336    This is the reverse of :func:`dump_header`.337 338    If a key does not have a value, it is ``None``.339 340    This handles charsets for values as described in341    `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,342    and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.343 344    :param value: The header value to parse.345 346    .. versionchanged:: 3.0347        Passing bytes is not supported.348 349    .. versionchanged:: 3.0350        The ``cls`` argument is removed.351 352    .. versionchanged:: 2.3353        Added support for ``key*=charset''value`` encoded items.354 355    .. versionchanged:: 0.9356       The ``cls`` argument was added.357    """358    result: dict[str, str | None] = {}359 360    for item in parse_list_header(value):361        key, has_value, value = item.partition("=")362        key = key.strip()363 364        if not has_value:365            result[key] = None366            continue367 368        value = value.strip()369        encoding: str | None = None370 371        if key[-1] == "*":372            # key*=charset''value becomes key=value, where value is percent encoded373            # adapted from parse_options_header, without the continuation handling374            key = key[:-1]375            match = _charset_value_re.match(value)376 377            if match:378                # If there is a charset marker in the value, split it off.379                encoding, value = match.groups()380                encoding = encoding.lower()381 382            # A safe list of encodings. Modern clients should only send ASCII or UTF-8.383            # This list will not be extended further. An invalid encoding will leave the384            # value quoted.385            if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:386                # invalid bytes are replaced during unquoting387                value = unquote(value, encoding=encoding)388 389        if len(value) >= 2 and value[0] == value[-1] == '"':390            value = value[1:-1]391 392        result[key] = value393 394    return result395 396 397# https://httpwg.org/specs/rfc9110.html#parameter398_parameter_re = re.compile(399    r"""400    # don't match multiple empty parts, that causes backtracking401    \s*;\s*  # find the part delimiter402    (?:403        ([\w!#$%&'*+\-.^`|~]+)  # key, one or more token chars404        =  # equals, with no space on either side405        (  # value, token or quoted string406            [\w!#$%&'*+\-.^`|~]+  # one or more token chars407        |408            "(?:\\\\|\\"|.)*?"  # quoted string, consuming slash escapes409        )410    )?  # optionally match key=value, to account for empty parts411    """,412    re.ASCII | re.VERBOSE,413)414# https://www.rfc-editor.org/rfc/rfc2231#section-4415_charset_value_re = re.compile(416    r"""417    ([\w!#$%&*+\-.^`|~]*)'  # charset part, could be empty418    [\w!#$%&*+\-.^`|~]*'  # don't care about language part, usually empty419    ([\w!#$%&'*+\-.^`|~]+)  # one or more token chars with percent encoding420    """,421    re.ASCII | re.VERBOSE,422)423# https://www.rfc-editor.org/rfc/rfc2231#section-3424_continuation_re = re.compile(r"\*(\d+)$", re.ASCII)425 426 427def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:428    """Parse a header that consists of a value with ``key=value`` parameters separated429    by semicolons ``;``. For example, the ``Content-Type`` header.430 431    .. code-block:: python432 433        parse_options_header("text/html; charset=UTF-8")434        ('text/html', {'charset': 'UTF-8'})435 436        parse_options_header("")437        ("", {})438 439    This is the reverse of :func:`dump_options_header`.440 441    This parses valid parameter parts as described in442    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are443    skipped.444 445    This handles continuations and charsets as described in446    `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as447    strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,448    otherwise the value remains quoted.449 450    Clients may not be consistent in how they handle a quote character within a quoted451    value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__452    replaces it with ``%22`` in multipart form data.453    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash454    escapes in HTTP headers. Both are decoded to the ``"`` character.455 456    Clients may not be consistent in how they handle non-ASCII characters. HTML457    documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with458    HTML character references, which can be decoded using :func:`html.unescape`.459 460    :param value: The header value to parse.461    :return: ``(value, options)``, where ``options`` is a dict462 463    .. versionchanged:: 2.3464        Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted465        values, are discarded instead of treating as ``None``.466 467    .. versionchanged:: 2.3468        Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.469 470    .. versionchanged:: 2.3471        Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.472 473    .. versionchanged:: 2.2474        Option names are always converted to lowercase.475 476    .. versionchanged:: 2.2477        The ``multiple`` parameter was removed.478 479    .. versionchanged:: 0.15480        :rfc:`2231` parameter continuations are handled.481 482    .. versionadded:: 0.5483    """484    if value is None:485        return "", {}486 487    value, _, rest = value.partition(";")488    value = value.strip()489    rest = rest.strip()490 491    if not value or not rest:492        # empty (invalid) value, or value without options493        return value, {}494 495    rest = f";{rest}"496    options: dict[str, str] = {}497    encoding: str | None = None498    continued_encoding: str | None = None499 500    for pk, pv in _parameter_re.findall(rest):501        if not pk:502            # empty or invalid part503            continue504 505        pk = pk.lower()506 507        if pk[-1] == "*":508            # key*=charset''value becomes key=value, where value is percent encoded509            pk = pk[:-1]510            match = _charset_value_re.match(pv)511 512            if match:513                # If there is a valid charset marker in the value, split it off.514                encoding, pv = match.groups()515                # This might be the empty string, handled next.516                encoding = encoding.lower()517 518            # No charset marker, or marker with empty charset value.519            if not encoding:520                encoding = continued_encoding521 522            # A safe list of encodings. Modern clients should only send ASCII or UTF-8.523            # This list will not be extended further. An invalid encoding will leave the524            # value quoted.525            if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:526                # Continuation parts don't require their own charset marker. This is527                # looser than the RFC, it will persist across different keys and allows528                # changing the charset during a continuation. But this implementation is529                # much simpler than tracking the full state.530                continued_encoding = encoding531                # invalid bytes are replaced during unquoting532                pv = unquote(pv, encoding=encoding)533 534        # Remove quotes. At this point the value cannot be empty or a single quote.535        if pv[0] == pv[-1] == '"':536            # HTTP headers use slash, multipart form data uses percent537            pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"')538 539        match = _continuation_re.search(pk)540 541        if match:542            # key*0=a; key*1=b becomes key=ab543            pk = pk[: match.start()]544            options[pk] = options.get(pk, "") + pv545        else:546            options[pk] = pv547 548    return value, options549 550 551_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII)552_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")553 554 555@t.overload556def parse_accept_header(value: str | None) -> ds.Accept: ...557 558 559@t.overload560def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ...561 562 563def parse_accept_header(564    value: str | None, cls: type[_TAnyAccept] | None = None565) -> _TAnyAccept:566    """Parse an ``Accept`` header according to567    `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__.568 569    Returns an :class:`.Accept` instance, which can sort and inspect items based on570    their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or571    ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.572 573    :param value: The header value to parse.574    :param cls: The :class:`.Accept` class to wrap the result in.575    :return: An instance of ``cls``.576 577    .. versionchanged:: 2.3578        Parse according to RFC 9110. Items with invalid ``q`` values are skipped.579    """580    if cls is None:581        cls = t.cast(t.Type[_TAnyAccept], ds.Accept)582 583    if not value:584        return cls(None)585 586    result = []587 588    for item in parse_list_header(value):589        item, options = parse_options_header(item)590 591        if "q" in options:592            # pop q, remaining options are reconstructed593            q_str = options.pop("q").strip()594 595            if _q_value_re.fullmatch(q_str) is None:596                # ignore an invalid q597                continue598 599            q = float(q_str)600 601            if q < 0 or q > 1:602                # ignore an invalid q603                continue604        else:605            q = 1606 607        if options:608            # reconstruct the media type with any options609            item = dump_options_header(item, options)610 611        result.append((item, q))612 613    return cls(result)614 615 616_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")617 618 619@t.overload620def parse_cache_control_header(621    value: str | None,622    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,623) -> ds.RequestCacheControl: ...624 625 626@t.overload627def parse_cache_control_header(628    value: str | None,629    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,630    cls: type[_TAnyCC] = ...,631) -> _TAnyCC: ...632 633 634def parse_cache_control_header(635    value: str | None,636    on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,637    cls: type[_TAnyCC] | None = None,638) -> _TAnyCC:639    """Parse a cache control header.  The RFC differs between response and640    request cache control, this method does not.  It's your responsibility641    to not use the wrong control statements.642 643    .. versionadded:: 0.5644       The `cls` was added.  If not specified an immutable645       :class:`~werkzeug.datastructures.RequestCacheControl` is returned.646 647    :param value: a cache control header to be parsed.648    :param on_update: an optional callable that is called every time a value649                      on the :class:`~werkzeug.datastructures.CacheControl`650                      object is changed.651    :param cls: the class for the returned object.  By default652                :class:`~werkzeug.datastructures.RequestCacheControl` is used.653    :return: a `cls` object.654    """655    if cls is None:656        cls = t.cast("type[_TAnyCC]", ds.RequestCacheControl)657 658    if not value:659        return cls((), on_update)660 661    return cls(parse_dict_header(value), on_update)662 663 664_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")665 666 667@t.overload668def parse_csp_header(669    value: str | None,670    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,671) -> ds.ContentSecurityPolicy: ...672 673 674@t.overload675def parse_csp_header(676    value: str | None,677    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,678    cls: type[_TAnyCSP] = ...,679) -> _TAnyCSP: ...680 681 682def parse_csp_header(683    value: str | None,684    on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,685    cls: type[_TAnyCSP] | None = None,686) -> _TAnyCSP:687    """Parse a Content Security Policy header.688 689    .. versionadded:: 1.0.0690       Support for Content Security Policy headers was added.691 692    :param value: a csp header to be parsed.693    :param on_update: an optional callable that is called every time a value694                      on the object is changed.695    :param cls: the class for the returned object.  By default696                :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.697    :return: a `cls` object.698    """699    if cls is None:700        cls = t.cast("type[_TAnyCSP]", ds.ContentSecurityPolicy)701 702    if value is None:703        return cls((), on_update)704 705    items = []706 707    for policy in value.split(";"):708        policy = policy.strip()709 710        # Ignore badly formatted policies (no space)711        if " " in policy:712            directive, value = policy.strip().split(" ", 1)713            items.append((directive.strip(), value.strip()))714 715    return cls(items, on_update)716 717 718def parse_set_header(719    value: str | None,720    on_update: t.Callable[[ds.HeaderSet], None] | None = None,721) -> ds.HeaderSet:722    """Parse a set-like header and return a723    :class:`~werkzeug.datastructures.HeaderSet` object:724 725    >>> hs = parse_set_header('token, "quoted value"')726 727    The return value is an object that treats the items case-insensitively728    and keeps the order of the items:729 730    >>> 'TOKEN' in hs731    True732    >>> hs.index('quoted value')733    1734    >>> hs735    HeaderSet(['token', 'quoted value'])736 737    To create a header from the :class:`HeaderSet` again, use the738    :func:`dump_header` function.739 740    :param value: a set header to be parsed.741    :param on_update: an optional callable that is called every time a742                      value on the :class:`~werkzeug.datastructures.HeaderSet`743                      object is changed.744    :return: a :class:`~werkzeug.datastructures.HeaderSet`745    """746    if not value:747        return ds.HeaderSet(None, on_update)748    return ds.HeaderSet(parse_list_header(value), on_update)749 750 751def parse_if_range_header(value: str | None) -> ds.IfRange:752    """Parses an if-range header which can be an etag or a date.  Returns753    a :class:`~werkzeug.datastructures.IfRange` object.754 755    .. versionchanged:: 2.0756        If the value represents a datetime, it is timezone-aware.757 758    .. versionadded:: 0.7759    """760    if not value:761        return ds.IfRange()762    date = parse_date(value)763    if date is not None:764        return ds.IfRange(date=date)765    # drop weakness information766    return ds.IfRange(unquote_etag(value)[0])767 768 769def parse_range_header(770    value: str | None, make_inclusive: bool = True771) -> ds.Range | None:772    """Parses a range header into a :class:`~werkzeug.datastructures.Range`773    object.  If the header is missing or malformed `None` is returned.774    `ranges` is a list of ``(start, stop)`` tuples where the ranges are775    non-inclusive.776 777    .. versionadded:: 0.7778    """779    if not value or "=" not in value:780        return None781 782    ranges = []783    last_end = 0784    units, rng = value.split("=", 1)785    units = units.strip().lower()786 787    for item in rng.split(","):788        item = item.strip()789        if "-" not in item:790            return None791        if item.startswith("-"):792            if last_end < 0:793                return None794            try:795                begin = _plain_int(item)796            except ValueError:797                return None798            end = None799            last_end = -1800        elif "-" in item:801            begin_str, end_str = item.split("-", 1)802            begin_str = begin_str.strip()803            end_str = end_str.strip()804 805            try:806                begin = _plain_int(begin_str)807            except ValueError:808                return None809 810            if begin < last_end or last_end < 0:811                return None812            if end_str:813                try:814                    end = _plain_int(end_str) + 1815                except ValueError:816                    return None817 818                if begin >= end:819                    return None820            else:821                end = None822            last_end = end if end is not None else -1823        ranges.append((begin, end))824 825    return ds.Range(units, ranges)826 827 828def parse_content_range_header(829    value: str | None,830    on_update: t.Callable[[ds.ContentRange], None] | None = None,831) -> ds.ContentRange | None:832    """Parses a range header into a833    :class:`~werkzeug.datastructures.ContentRange` object or `None` if834    parsing is not possible.835 836    .. versionadded:: 0.7837 838    :param value: a content range header to be parsed.839    :param on_update: an optional callable that is called every time a value840                      on the :class:`~werkzeug.datastructures.ContentRange`841                      object is changed.842    """843    if value is None:844        return None845    try:846        units, rangedef = (value or "").strip().split(None, 1)847    except ValueError:848        return None849 850    if "/" not in rangedef:851        return None852    rng, length_str = rangedef.split("/", 1)853    if length_str == "*":854        length = None855    else:856        try:857            length = _plain_int(length_str)858        except ValueError:859            return None860 861    if rng == "*":862        if not is_byte_range_valid(None, None, length):863            return None864 865        return ds.ContentRange(units, None, None, length, on_update=on_update)866    elif "-" not in rng:867        return None868 869    start_str, stop_str = rng.split("-", 1)870    try:871        start = _plain_int(start_str)872        stop = _plain_int(stop_str) + 1873    except ValueError:874        return None875 876    if is_byte_range_valid(start, stop, length):877        return ds.ContentRange(units, start, stop, length, on_update=on_update)878 879    return None880 881 882def quote_etag(etag: str, weak: bool = False) -> str:883    """Quote an etag.884 885    :param etag: the etag to quote.886    :param weak: set to `True` to tag it "weak".887    """888    if '"' in etag:889        raise ValueError("invalid etag")890    etag = f'"{etag}"'891    if weak:892        etag = f"W/{etag}"893    return etag894 895 896def unquote_etag(897    etag: str | None,898) -> tuple[str, bool] | tuple[None, None]:899    """Unquote a single etag:900 901    >>> unquote_etag('W/"bar"')902    ('bar', True)903    >>> unquote_etag('"bar"')904    ('bar', False)905 906    :param etag: the etag identifier to unquote.907    :return: a ``(etag, weak)`` tuple.908    """909    if not etag:910        return None, None911    etag = etag.strip()912    weak = False913    if etag.startswith(("W/", "w/")):914        weak = True915        etag = etag[2:]916    if etag[:1] == etag[-1:] == '"':917        etag = etag[1:-1]918    return etag, weak919 920 921def parse_etags(value: str | None) -> ds.ETags:922    """Parse an etag header.923 924    :param value: the tag header to parse925    :return: an :class:`~werkzeug.datastructures.ETags` object.926    """927    if not value:928        return ds.ETags()929    strong = []930    weak = []931    end = len(value)932    pos = 0933    while pos < end:934        match = _etag_re.match(value, pos)935        if match is None:936            break937        is_weak, quoted, raw = match.groups()938        if raw == "*":939            return ds.ETags(star_tag=True)940        elif quoted:941            raw = quoted942        if is_weak:943            weak.append(raw)944        else:945            strong.append(raw)946        pos = match.end()947    return ds.ETags(strong, weak)948 949 950def generate_etag(data: bytes) -> str:951    """Generate an etag for some data.952 953    .. versionchanged:: 2.0954        Use SHA-1. MD5 may not be available in some environments.955    """956    return sha1(data).hexdigest()957 958 959def parse_date(value: str | None) -> datetime | None:960    """Parse an :rfc:`2822` date into a timezone-aware961    :class:`datetime.datetime` object, or ``None`` if parsing fails.962 963    This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It964    returns ``None`` if parsing fails instead of raising an exception,965    and always returns a timezone-aware datetime object. If the string966    doesn't have timezone information, it is assumed to be UTC.967 968    :param value: A string with a supported date format.969 970    .. versionchanged:: 2.0971        Return a timezone-aware datetime object. Use972        ``email.utils.parsedate_to_datetime``.973    """974    if value is None:975        return None976 977    try:978        dt = email.utils.parsedate_to_datetime(value)979    except (TypeError, ValueError):980        return None981 982    if dt.tzinfo is None:983        return dt.replace(tzinfo=timezone.utc)984 985    return dt986 987 988def http_date(989    timestamp: datetime | date | int | float | struct_time | None = None,990) -> str:991    """Format a datetime object or timestamp into an :rfc:`2822` date992    string.993 994    This is a wrapper for :func:`email.utils.format_datetime`. It995    assumes naive datetime objects are in UTC instead of raising an996    exception.997 998    :param timestamp: The datetime or timestamp to format. Defaults to999        the current time.1000 1001    .. versionchanged:: 2.01002        Use ``email.utils.format_datetime``. Accept ``date`` objects.1003    """1004    if isinstance(timestamp, date):1005        if not isinstance(timestamp, datetime):1006            # Assume plain date is midnight UTC.1007            timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc)1008        else:1009            # Ensure datetime is timezone-aware.1010            timestamp = _dt_as_utc(timestamp)1011 1012        return email.utils.format_datetime(timestamp, usegmt=True)1013 1014    if isinstance(timestamp, struct_time):1015        timestamp = mktime(timestamp)1016 1017    return email.utils.formatdate(timestamp, usegmt=True)1018 1019 1020def parse_age(value: str | None = None) -> timedelta | None:1021    """Parses a base-10 integer count of seconds into a timedelta.1022 1023    If parsing fails, the return value is `None`.1024 1025    :param value: a string consisting of an integer represented in base-101026    :return: a :class:`datetime.timedelta` object or `None`.1027    """1028    if not value:1029        return None1030    try:1031        seconds = int(value)1032    except ValueError:1033        return None1034    if seconds < 0:1035        return None1036    try:1037        return timedelta(seconds=seconds)1038    except OverflowError:1039        return None1040 1041 1042def dump_age(age: timedelta | int | None = None) -> str | None:1043    """Formats the duration as a base-10 integer.1044 1045    :param age: should be an integer number of seconds,1046                a :class:`datetime.timedelta` object, or,1047                if the age is unknown, `None` (default).1048    """1049    if age is None:1050        return None1051    if isinstance(age, timedelta):1052        age = int(age.total_seconds())1053    else:1054        age = int(age)1055 1056    if age < 0:1057        raise ValueError("age cannot be negative")1058 1059    return str(age)1060 1061 1062def is_resource_modified(1063    environ: WSGIEnvironment,1064    etag: str | None = None,1065    data: bytes | None = None,1066    last_modified: datetime | str | None = None,1067    ignore_if_range: bool = True,1068) -> bool:1069    """Convenience method for conditional requests.1070 1071    :param environ: the WSGI environment of the request to be checked.1072    :param etag: the etag for the response for comparison.1073    :param data: or alternatively the data of the response to automatically1074                 generate an etag using :func:`generate_etag`.1075    :param last_modified: an optional date of the last modification.1076    :param ignore_if_range: If `False`, `If-Range` header will be taken into1077                            account.1078    :return: `True` if the resource was modified, otherwise `False`.1079 1080    .. versionchanged:: 2.01081        SHA-1 is used to generate an etag value for the data. MD5 may1082        not be available in some environments.1083 1084    .. versionchanged:: 1.0.01085        The check is run for methods other than ``GET`` and ``HEAD``.1086    """1087    return _sansio_http.is_resource_modified(1088        http_range=environ.get("HTTP_RANGE"),1089        http_if_range=environ.get("HTTP_IF_RANGE"),1090        http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),1091        http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),1092        http_if_match=environ.get("HTTP_IF_MATCH"),1093        etag=etag,1094        data=data,1095        last_modified=last_modified,1096        ignore_if_range=ignore_if_range,1097    )1098 1099 1100def remove_entity_headers(1101    headers: ds.Headers | list[tuple[str, str]],1102    allowed: t.Iterable[str] = ("expires", "content-location"),1103) -> None:1104    """Remove all entity headers from a list or :class:`Headers` object.  This1105    operation works in-place.  `Expires` and `Content-Location` headers are1106    by default not removed.  The reason for this is :rfc:`2616` section1107    10.3.5 which specifies some entity headers that should be sent.1108 1109    .. versionchanged:: 0.51110       added `allowed` parameter.1111 1112    :param headers: a list or :class:`Headers` object.1113    :param allowed: a list of headers that should still be allowed even though1114                    they are entity headers.1115    """1116    allowed = {x.lower() for x in allowed}1117    headers[:] = [1118        (key, value)1119        for key, value in headers1120        if not is_entity_header(key) or key.lower() in allowed1121    ]1122 1123 1124def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None:1125    """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or1126    :class:`Headers` object.  This operation works in-place.1127 1128    .. versionadded:: 0.51129 1130    :param headers: a list or :class:`Headers` object.1131    """1132    headers[:] = [1133        (key, value) for key, value in headers if not is_hop_by_hop_header(key)1134    ]1135 1136 1137def is_entity_header(header: str) -> bool:1138    """Check if a header is an entity header.1139 1140    .. versionadded:: 0.51141 1142    :param header: the header to test.1143    :return: `True` if it's an entity header, `False` otherwise.1144    """1145    return header.lower() in _entity_headers1146 1147 1148def is_hop_by_hop_header(header: str) -> bool:1149    """Check if a header is an HTTP/1.1 "Hop-by-Hop" header.1150 1151    .. versionadded:: 0.51152 1153    :param header: the header to test.1154    :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise.1155    """1156    return header.lower() in _hop_by_hop_headers1157 1158 1159def parse_cookie(1160    header: WSGIEnvironment | str | None,1161    cls: type[ds.MultiDict[str, str]] | None = None,1162) -> ds.MultiDict[str, str]:1163    """Parse a cookie from a string or WSGI environ.1164 1165    The same key can be provided multiple times, the values are stored1166    in-order. The default :class:`MultiDict` will have the first value1167    first, and all values can be retrieved with1168    :meth:`MultiDict.getlist`.1169 1170    :param header: The cookie header as a string, or a WSGI environ dict1171        with a ``HTTP_COOKIE`` key.1172    :param cls: A dict-like class to store the parsed cookies in.1173        Defaults to :class:`MultiDict`.1174 1175    .. versionchanged:: 3.01176        Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.1177 1178    .. versionchanged:: 1.01179        Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.1180 1181    .. versionchanged:: 0.51182        Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls``1183        parameter was added.1184    """1185    if isinstance(header, dict):1186        cookie = header.get("HTTP_COOKIE")1187    else:1188        cookie = header1189 1190    if cookie:1191        cookie = cookie.encode("latin1").decode()1192 1193    return _sansio_http.parse_cookie(cookie=cookie, cls=cls)1194 1195 1196_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)1197_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)1198_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}1199_cookie_slash_map.update(1200    (v.to_bytes(1, "big"), b"\\%03o" % v)

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

codekingpro/portable-devtools · Team Ai