Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
response.py755 linesDownload Raw Back to sansio
1from __future__ import annotations2 3import typing as t4from datetime import datetime5from datetime import timedelta6from datetime import timezone7from http import HTTPStatus8 9from ..datastructures import CallbackDict10from ..datastructures import ContentRange11from ..datastructures import ContentSecurityPolicy12from ..datastructures import Headers13from ..datastructures import HeaderSet14from ..datastructures import ResponseCacheControl15from ..datastructures import WWWAuthenticate16from ..http import COEP17from ..http import COOP18from ..http import dump_age19from ..http import dump_cookie20from ..http import dump_header21from ..http import dump_options_header22from ..http import http_date23from ..http import HTTP_STATUS_CODES24from ..http import parse_age25from ..http import parse_cache_control_header26from ..http import parse_content_range_header27from ..http import parse_csp_header28from ..http import parse_date29from ..http import parse_options_header30from ..http import parse_set_header31from ..http import quote_etag32from ..http import unquote_etag33from ..utils import get_content_type34from ..utils import header_property35 36if t.TYPE_CHECKING:37    from ..datastructures.cache_control import _CacheControl38 39 40def _set_property(name: str, doc: str | None = None) -> property:41    def fget(self: Response) -> HeaderSet:42        def on_update(header_set: HeaderSet) -> None:43            if not header_set and name in self.headers:44                del self.headers[name]45            elif header_set:46                self.headers[name] = header_set.to_header()47 48        return parse_set_header(self.headers.get(name), on_update)49 50    def fset(51        self: Response,52        value: None | (str | dict[str, str | int] | t.Iterable[str]),53    ) -> None:54        if not value:55            del self.headers[name]56        elif isinstance(value, str):57            self.headers[name] = value58        else:59            self.headers[name] = dump_header(value)60 61    return property(fget, fset, doc=doc)62 63 64class Response:65    """Represents the non-IO parts of an HTTP response, specifically the66    status and headers but not the body.67 68    This class is not meant for general use. It should only be used when69    implementing WSGI, ASGI, or another HTTP application spec. Werkzeug70    provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`.71 72    :param status: The status code for the response. Either an int, in73        which case the default status message is added, or a string in74        the form ``{code} {message}``, like ``404 Not Found``. Defaults75        to 200.76    :param headers: A :class:`~werkzeug.datastructures.Headers` object,77        or a list of ``(key, value)`` tuples that will be converted to a78        ``Headers`` object.79    :param mimetype: The mime type (content type without charset or80        other parameters) of the response. If the value starts with81        ``text/`` (or matches some other special cases), the charset82        will be added to create the ``content_type``.83    :param content_type: The full content type of the response.84        Overrides building the value from ``mimetype``.85 86    .. versionchanged:: 3.087        The ``charset`` attribute was removed.88 89    .. versionadded:: 2.090    """91 92    #: the default status if none is provided.93    default_status = 20094 95    #: the default mimetype if none is provided.96    default_mimetype: str | None = "text/plain"97 98    #: Warn if a cookie header exceeds this size. The default, 4093, should be99    #: safely `supported by most browsers <cookie_>`_. A cookie larger than100    #: this size will still be sent, but it may be ignored or handled101    #: incorrectly by some browsers. Set to 0 to disable this check.102    #:103    #: .. versionadded:: 0.13104    #:105    #: .. _`cookie`: http://browsercookielimits.squawky.net/106    max_cookie_size = 4093107 108    # A :class:`Headers` object representing the response headers.109    headers: Headers110 111    def __init__(112        self,113        status: int | str | HTTPStatus | None = None,114        headers: t.Mapping[str, str | t.Iterable[str]]115        | t.Iterable[tuple[str, str]]116        | None = None,117        mimetype: str | None = None,118        content_type: str | None = None,119    ) -> None:120        if isinstance(headers, Headers):121            self.headers = headers122        elif not headers:123            self.headers = Headers()124        else:125            self.headers = Headers(headers)126 127        if content_type is None:128            if mimetype is None and "content-type" not in self.headers:129                mimetype = self.default_mimetype130            if mimetype is not None:131                mimetype = get_content_type(mimetype, "utf-8")132            content_type = mimetype133        if content_type is not None:134            self.headers["Content-Type"] = content_type135        if status is None:136            status = self.default_status137        self.status = status  # type: ignore138 139    def __repr__(self) -> str:140        return f"<{type(self).__name__} [{self.status}]>"141 142    @property143    def status_code(self) -> int:144        """The HTTP status code as a number."""145        return self._status_code146 147    @status_code.setter148    def status_code(self, code: int) -> None:149        self.status = code  # type: ignore150 151    @property152    def status(self) -> str:153        """The HTTP status code as a string."""154        return self._status155 156    @status.setter157    def status(self, value: str | int | HTTPStatus) -> None:158        self._status, self._status_code = self._clean_status(value)159 160    def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]:161        if isinstance(value, (int, HTTPStatus)):162            status_code = int(value)163        else:164            value = value.strip()165 166            if not value:167                raise ValueError("Empty status argument")168 169            code_str, sep, _ = value.partition(" ")170 171            try:172                status_code = int(code_str)173            except ValueError:174                # only message175                return f"0 {value}", 0176 177            if sep:178                # code and message179                return value, status_code180 181        # only code, look up message182        try:183            status = f"{status_code} {HTTP_STATUS_CODES[status_code].upper()}"184        except KeyError:185            status = f"{status_code} UNKNOWN"186 187        return status, status_code188 189    def set_cookie(190        self,191        key: str,192        value: str = "",193        max_age: timedelta | int | None = None,194        expires: str | datetime | int | float | None = None,195        path: str | None = "/",196        domain: str | None = None,197        secure: bool = False,198        httponly: bool = False,199        samesite: str | None = None,200    ) -> None:201        """Sets a cookie.202 203        A warning is raised if the size of the cookie header exceeds204        :attr:`max_cookie_size`, but the header will still be set.205 206        :param key: the key (name) of the cookie to be set.207        :param value: the value of the cookie.208        :param max_age: should be a number of seconds, or `None` (default) if209                        the cookie should last only as long as the client's210                        browser session.211        :param expires: should be a `datetime` object or UNIX timestamp.212        :param path: limits the cookie to a given path, per default it will213                     span the whole domain.214        :param domain: if you want to set a cross-domain cookie.  For example,215                       ``domain="example.com"`` will set a cookie that is216                       readable by the domain ``www.example.com``,217                       ``foo.example.com`` etc.  Otherwise, a cookie will only218                       be readable by the domain that set it.219        :param secure: If ``True``, the cookie will only be available220            via HTTPS.221        :param httponly: Disallow JavaScript access to the cookie.222        :param samesite: Limit the scope of the cookie to only be223            attached to requests that are "same-site".224        """225        self.headers.add(226            "Set-Cookie",227            dump_cookie(228                key,229                value=value,230                max_age=max_age,231                expires=expires,232                path=path,233                domain=domain,234                secure=secure,235                httponly=httponly,236                max_size=self.max_cookie_size,237                samesite=samesite,238            ),239        )240 241    def delete_cookie(242        self,243        key: str,244        path: str | None = "/",245        domain: str | None = None,246        secure: bool = False,247        httponly: bool = False,248        samesite: str | None = None,249    ) -> None:250        """Delete a cookie.  Fails silently if key doesn't exist.251 252        :param key: the key (name) of the cookie to be deleted.253        :param path: if the cookie that should be deleted was limited to a254                     path, the path has to be defined here.255        :param domain: if the cookie that should be deleted was limited to a256                       domain, that domain has to be defined here.257        :param secure: If ``True``, the cookie will only be available258            via HTTPS.259        :param httponly: Disallow JavaScript access to the cookie.260        :param samesite: Limit the scope of the cookie to only be261            attached to requests that are "same-site".262        """263        self.set_cookie(264            key,265            expires=0,266            max_age=0,267            path=path,268            domain=domain,269            secure=secure,270            httponly=httponly,271            samesite=samesite,272        )273 274    @property275    def is_json(self) -> bool:276        """Check if the mimetype indicates JSON data, either277        :mimetype:`application/json` or :mimetype:`application/*+json`.278        """279        mt = self.mimetype280        return mt is not None and (281            mt == "application/json"282            or mt.startswith("application/")283            and mt.endswith("+json")284        )285 286    # Common Descriptors287 288    @property289    def mimetype(self) -> str | None:290        """The mimetype (content type without charset etc.)"""291        ct = self.headers.get("content-type")292 293        if ct:294            return ct.split(";")[0].strip()295        else:296            return None297 298    @mimetype.setter299    def mimetype(self, value: str) -> None:300        self.headers["Content-Type"] = get_content_type(value, "utf-8")301 302    @property303    def mimetype_params(self) -> dict[str, str]:304        """The mimetype parameters as dict. For example if the305        content type is ``text/html; charset=utf-8`` the params would be306        ``{'charset': 'utf-8'}``.307 308        .. versionadded:: 0.5309        """310 311        def on_update(d: CallbackDict[str, str]) -> None:312            self.headers["Content-Type"] = dump_options_header(self.mimetype, d)313 314        d = parse_options_header(self.headers.get("content-type", ""))[1]315        return CallbackDict(d, on_update)316 317    location = header_property[str](318        "Location",319        doc="""The Location response-header field is used to redirect320        the recipient to a location other than the Request-URI for321        completion of the request or identification of a new322        resource.""",323    )324    age = header_property(325        "Age",326        None,327        parse_age,328        dump_age,  # type: ignore329        doc="""The Age response-header field conveys the sender's330        estimate of the amount of time since the response (or its331        revalidation) was generated at the origin server.332 333        Age values are non-negative decimal integers, representing time334        in seconds.""",335    )336    content_type = header_property[str](337        "Content-Type",338        doc="""The Content-Type entity-header field indicates the media339        type of the entity-body sent to the recipient or, in the case of340        the HEAD method, the media type that would have been sent had341        the request been a GET.""",342    )343    content_length = header_property(344        "Content-Length",345        None,346        int,347        str,348        doc="""The Content-Length entity-header field indicates the size349        of the entity-body, in decimal number of OCTETs, sent to the350        recipient or, in the case of the HEAD method, the size of the351        entity-body that would have been sent had the request been a352        GET.""",353    )354    content_location = header_property[str](355        "Content-Location",356        doc="""The Content-Location entity-header field MAY be used to357        supply the resource location for the entity enclosed in the358        message when that entity is accessible from a location separate359        from the requested resource's URI.""",360    )361    content_encoding = header_property[str](362        "Content-Encoding",363        doc="""The Content-Encoding entity-header field is used as a364        modifier to the media-type. When present, its value indicates365        what additional content codings have been applied to the366        entity-body, and thus what decoding mechanisms must be applied367        in order to obtain the media-type referenced by the Content-Type368        header field.""",369    )370    content_md5 = header_property[str](371        "Content-MD5",372        doc="""The Content-MD5 entity-header field, as defined in373        RFC 1864, is an MD5 digest of the entity-body for the purpose of374        providing an end-to-end message integrity check (MIC) of the375        entity-body. (Note: a MIC is good for detecting accidental376        modification of the entity-body in transit, but is not proof377        against malicious attacks.)""",378    )379    date = header_property(380        "Date",381        None,382        parse_date,383        http_date,384        doc="""The Date general-header field represents the date and385        time at which the message was originated, having the same386        semantics as orig-date in RFC 822.387 388        .. versionchanged:: 2.0389            The datetime object is timezone-aware.390        """,391    )392    expires = header_property(393        "Expires",394        None,395        parse_date,396        http_date,397        doc="""The Expires entity-header field gives the date/time after398        which the response is considered stale. A stale cache entry may399        not normally be returned by a cache.400 401        .. versionchanged:: 2.0402            The datetime object is timezone-aware.403        """,404    )405    last_modified = header_property(406        "Last-Modified",407        None,408        parse_date,409        http_date,410        doc="""The Last-Modified entity-header field indicates the date411        and time at which the origin server believes the variant was412        last modified.413 414        .. versionchanged:: 2.0415            The datetime object is timezone-aware.416        """,417    )418 419    @property420    def retry_after(self) -> datetime | None:421        """The Retry-After response-header field can be used with a422        503 (Service Unavailable) response to indicate how long the423        service is expected to be unavailable to the requesting client.424 425        Time in seconds until expiration or date.426 427        .. versionchanged:: 2.0428            The datetime object is timezone-aware.429        """430        value = self.headers.get("retry-after")431        if value is None:432            return None433 434        try:435            seconds = int(value)436        except ValueError:437            return parse_date(value)438 439        return datetime.now(timezone.utc) + timedelta(seconds=seconds)440 441    @retry_after.setter442    def retry_after(self, value: datetime | int | str | None) -> None:443        if value is None:444            if "retry-after" in self.headers:445                del self.headers["retry-after"]446            return447        elif isinstance(value, datetime):448            value = http_date(value)449        else:450            value = str(value)451        self.headers["Retry-After"] = value452 453    vary = _set_property(454        "Vary",455        doc="""The Vary field value indicates the set of request-header456        fields that fully determines, while the response is fresh,457        whether a cache is permitted to use the response to reply to a458        subsequent request without revalidation.""",459    )460    content_language = _set_property(461        "Content-Language",462        doc="""The Content-Language entity-header field describes the463        natural language(s) of the intended audience for the enclosed464        entity. Note that this might not be equivalent to all the465        languages used within the entity-body.""",466    )467    allow = _set_property(468        "Allow",469        doc="""The Allow entity-header field lists the set of methods470        supported by the resource identified by the Request-URI. The471        purpose of this field is strictly to inform the recipient of472        valid methods associated with the resource. An Allow header473        field MUST be present in a 405 (Method Not Allowed)474        response.""",475    )476 477    # ETag478 479    @property480    def cache_control(self) -> ResponseCacheControl:481        """The Cache-Control general-header field is used to specify482        directives that MUST be obeyed by all caching mechanisms along the483        request/response chain.484        """485 486        def on_update(cache_control: _CacheControl) -> None:487            if not cache_control and "cache-control" in self.headers:488                del self.headers["cache-control"]489            elif cache_control:490                self.headers["Cache-Control"] = cache_control.to_header()491 492        return parse_cache_control_header(493            self.headers.get("cache-control"), on_update, ResponseCacheControl494        )495 496    def set_etag(self, etag: str, weak: bool = False) -> None:497        """Set the etag, and override the old one if there was one."""498        self.headers["ETag"] = quote_etag(etag, weak)499 500    def get_etag(self) -> tuple[str, bool] | tuple[None, None]:501        """Return a tuple in the form ``(etag, is_weak)``.  If there is no502        ETag the return value is ``(None, None)``.503        """504        return unquote_etag(self.headers.get("ETag"))505 506    accept_ranges = header_property[str](507        "Accept-Ranges",508        doc="""The `Accept-Ranges` header. Even though the name would509        indicate that multiple values are supported, it must be one510        string token only.511 512        The values ``'bytes'`` and ``'none'`` are common.513 514        .. versionadded:: 0.7""",515    )516 517    @property518    def content_range(self) -> ContentRange:519        """The ``Content-Range`` header as a520        :class:`~werkzeug.datastructures.ContentRange` object. Available521        even if the header is not set.522 523        .. versionadded:: 0.7524        """525 526        def on_update(rng: ContentRange) -> None:527            if not rng:528                del self.headers["content-range"]529            else:530                self.headers["Content-Range"] = rng.to_header()531 532        rv = parse_content_range_header(self.headers.get("content-range"), on_update)533        # always provide a content range object to make the descriptor534        # more user friendly.  It provides an unset() method that can be535        # used to remove the header quickly.536        if rv is None:537            rv = ContentRange(None, None, None, on_update=on_update)538        return rv539 540    @content_range.setter541    def content_range(self, value: ContentRange | str | None) -> None:542        if not value:543            del self.headers["content-range"]544        elif isinstance(value, str):545            self.headers["Content-Range"] = value546        else:547            self.headers["Content-Range"] = value.to_header()548 549    # Authorization550 551    @property552    def www_authenticate(self) -> WWWAuthenticate:553        """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate`554        object. Modifying the object will modify the header value.555 556        This header is not set by default. To set this header, assign an instance of557        :class:`.WWWAuthenticate` to this attribute.558 559        .. code-block:: python560 561            response.www_authenticate = WWWAuthenticate(562                "basic", {"realm": "Authentication Required"}563            )564 565        Multiple values for this header can be sent to give the client multiple options.566        Assign a list to set multiple headers. However, modifying the items in the list567        will not automatically update the header values, and accessing this attribute568        will only ever return the first value.569 570        To unset this header, assign ``None`` or use ``del``.571 572        .. versionchanged:: 2.3573            This attribute can be assigned to to set the header. A list can be assigned574            to set multiple header values. Use ``del`` to unset the header.575 576        .. versionchanged:: 2.3577            :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute578            was added for auth challenges that use a token instead of parameters.579        """580        value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate"))581 582        if value is None:583            value = WWWAuthenticate("basic")584 585        def on_update(value: WWWAuthenticate) -> None:586            self.www_authenticate = value587 588        value._on_update = on_update589        return value590 591    @www_authenticate.setter592    def www_authenticate(593        self, value: WWWAuthenticate | list[WWWAuthenticate] | None594    ) -> None:595        if not value:  # None or empty list596            del self.www_authenticate597        elif isinstance(value, list):598            # Clear any existing header by setting the first item.599            self.headers.set("WWW-Authenticate", value[0].to_header())600 601            for item in value[1:]:602                # Add additional header lines for additional items.603                self.headers.add("WWW-Authenticate", item.to_header())604        else:605            self.headers.set("WWW-Authenticate", value.to_header())606 607            def on_update(value: WWWAuthenticate) -> None:608                self.www_authenticate = value609 610            # When setting a single value, allow updating it directly.611            value._on_update = on_update612 613    @www_authenticate.deleter614    def www_authenticate(self) -> None:615        if "WWW-Authenticate" in self.headers:616            del self.headers["WWW-Authenticate"]617 618    # CSP619 620    @property621    def content_security_policy(self) -> ContentSecurityPolicy:622        """The ``Content-Security-Policy`` header as a623        :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available624        even if the header is not set.625 626        The Content-Security-Policy header adds an additional layer of627        security to help detect and mitigate certain types of attacks.628        """629 630        def on_update(csp: ContentSecurityPolicy) -> None:631            if not csp:632                del self.headers["content-security-policy"]633            else:634                self.headers["Content-Security-Policy"] = csp.to_header()635 636        rv = parse_csp_header(self.headers.get("content-security-policy"), on_update)637        if rv is None:638            rv = ContentSecurityPolicy(None, on_update=on_update)639        return rv640 641    @content_security_policy.setter642    def content_security_policy(643        self, value: ContentSecurityPolicy | str | None644    ) -> None:645        if not value:646            del self.headers["content-security-policy"]647        elif isinstance(value, str):648            self.headers["Content-Security-Policy"] = value649        else:650            self.headers["Content-Security-Policy"] = value.to_header()651 652    @property653    def content_security_policy_report_only(self) -> ContentSecurityPolicy:654        """The ``Content-Security-policy-report-only`` header as a655        :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available656        even if the header is not set.657 658        The Content-Security-Policy-Report-Only header adds a csp policy659        that is not enforced but is reported thereby helping detect660        certain types of attacks.661        """662 663        def on_update(csp: ContentSecurityPolicy) -> None:664            if not csp:665                del self.headers["content-security-policy-report-only"]666            else:667                self.headers["Content-Security-policy-report-only"] = csp.to_header()668 669        rv = parse_csp_header(670            self.headers.get("content-security-policy-report-only"), on_update671        )672        if rv is None:673            rv = ContentSecurityPolicy(None, on_update=on_update)674        return rv675 676    @content_security_policy_report_only.setter677    def content_security_policy_report_only(678        self, value: ContentSecurityPolicy | str | None679    ) -> None:680        if not value:681            del self.headers["content-security-policy-report-only"]682        elif isinstance(value, str):683            self.headers["Content-Security-policy-report-only"] = value684        else:685            self.headers["Content-Security-policy-report-only"] = value.to_header()686 687    # CORS688 689    @property690    def access_control_allow_credentials(self) -> bool:691        """Whether credentials can be shared by the browser to692        JavaScript code. As part of the preflight request it indicates693        whether credentials can be used on the cross origin request.694        """695        return "Access-Control-Allow-Credentials" in self.headers696 697    @access_control_allow_credentials.setter698    def access_control_allow_credentials(self, value: bool | None) -> None:699        if value is True:700            self.headers["Access-Control-Allow-Credentials"] = "true"701        else:702            self.headers.pop("Access-Control-Allow-Credentials", None)703 704    access_control_allow_headers = header_property(705        "Access-Control-Allow-Headers",706        load_func=parse_set_header,707        dump_func=dump_header,708        doc="Which headers can be sent with the cross origin request.",709    )710 711    access_control_allow_methods = header_property(712        "Access-Control-Allow-Methods",713        load_func=parse_set_header,714        dump_func=dump_header,715        doc="Which methods can be used for the cross origin request.",716    )717 718    access_control_allow_origin = header_property[str](719        "Access-Control-Allow-Origin",720        doc="The origin or '*' for any origin that may make cross origin requests.",721    )722 723    access_control_expose_headers = header_property(724        "Access-Control-Expose-Headers",725        load_func=parse_set_header,726        dump_func=dump_header,727        doc="Which headers can be shared by the browser to JavaScript code.",728    )729 730    access_control_max_age = header_property(731        "Access-Control-Max-Age",732        load_func=int,733        dump_func=str,734        doc="The maximum age in seconds the access control settings can be cached for.",735    )736 737    cross_origin_opener_policy = header_property[COOP](738        "Cross-Origin-Opener-Policy",739        load_func=lambda value: COOP(value),740        dump_func=lambda value: value.value,741        default=COOP.UNSAFE_NONE,742        doc="""Allows control over sharing of browsing context group with cross-origin743        documents. Values must be a member of the :class:`werkzeug.http.COOP` enum.""",744    )745 746    cross_origin_embedder_policy = header_property[COEP](747        "Cross-Origin-Embedder-Policy",748        load_func=lambda value: COEP(value),749        dump_func=lambda value: value.value,750        default=COEP.UNSAFE_NONE,751        doc="""Prevents a document from loading any cross-origin resources that do not752        explicitly grant the document permission. Values must be a member of the753        :class:`werkzeug.http.COEP` enum.""",754    )755 
codekingpro/portable-devtools · Team Ai