Source code for dmr.cookies

# Parts of the code is taken from
# https://github.com/litestar-org/litestar/blob/main/litestar/datastructures/cookie.py
# under MIT license.

# Original license:
# https://github.com/litestar-org/litestar/blob/main/LICENSE

# The MIT License (MIT)

# Copyright (c) 2021, 2022, 2023, 2024, 2025, 2026 Litestar Org.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import dataclasses
import datetime as dt
from collections.abc import Mapping
from http.cookies import Morsel, SimpleCookie
from typing import (
    Any,
    ClassVar,
    Final,
    Literal,
    TypeAlias,
    final,
)

from django.http import HttpResponseBase

from dmr.internal.types import StrOrPromise

#: Type alias for possible `samesite` values.
SameSite: TypeAlias = Literal['lax', 'strict', 'none']


@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class _BaseCookie:
    """Base class for all cookies."""

    path: StrOrPromise = '/'
    max_age: int | None = None
    expires: int | dt.datetime | None = None
    domain: str | None = None
    secure: bool = False
    httponly: bool = False
    samesite: SameSite = 'lax'


[docs] @final @dataclasses.dataclass(frozen=True, slots=True, kw_only=True) class CookieSpec(_BaseCookie): """ Description of a single cookie in ``Set-Cookie`` header. Attributes: path: Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. Can be a lazy string, so a cookie can be scoped to a :func:`django.urls.reverse_lazy` url. max_age: Maximal age of the cookie before its invalidated. expires: Seconds from now until the cookie expires. domain: Domain for which the cookie is valid. secure: Https is required for the cookie. httponly: Forbids javascript to access the cookie via ``document.cookie``. samesite: Controls whether or not a cookie is sent with cross-site requests. Defaults to ``'lax'``. description: Description of the response cookie header for OpenAPI documentation. required: Defines that this cookie can be missing in some cases. skip_validation: Is true, when cookie is only used for schema purposes, without any runtime validation. This might be useful, when this cookie will be set after our framework's validation. For example, by :class:`django.contrib.sessions.middleware.SessionMiddleware` or by HTTP proxy. This cookie might be present in runtime or might be missing. .. seealso:: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie """ is_actionable: ClassVar[Literal[False]] = False description: StrOrPromise | None = None required: bool = True skip_validation: bool = False
[docs] def is_equal(self, other: Morsel[str]) -> bool: """Compare this object with ``SimpleCookie`` like object.""" # We have already compared keys, value is not important. cookie = SimpleCookie() cookie[other.key] = other.value namespace = cookie[other.key] for field in _COOKIE_SPEC_FIELDS: field_name, field_value = self._morsel_field(field, other) namespace[field_name] = field_value return cookie[other.key] == other
[docs] def to_spec(self) -> 'CookieSpec': """API for compatibility with ``NewCookie``.""" return self
def _morsel_field( self, field_name: str, other: Morsel[str], ) -> tuple[str, Any]: if field_name == 'expires': # It is relative to the current time, can't check it. return field_name, other[field_name] if field_name == 'max_age': # `0` is a real value here: it tells the browser to drop # the cookie right away. So, unlike all the other fields, # we cannot treat it as a missing one. return 'max-age', '' if self.max_age is None else self.max_age return field_name, getattr(self, field_name) or ''
#: We use module level fields not to calculate them each time. _COOKIE_SPEC_FIELDS: Final = frozenset( field.name for field in dataclasses.fields(CookieSpec) if field.name not in {'description', 'required', 'skip_validation'} )
[docs] @final @dataclasses.dataclass(frozen=True, slots=True, kw_only=True) class NewCookie(_BaseCookie): """ New cookie to be set for the response. Attributes: value: Value for the cookie. path: Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. Can be a lazy string, so a cookie can be scoped to a :func:`django.urls.reverse_lazy` url. max_age: Maximal age of the cookie before its invalidated. expires: Seconds from now until the cookie expires. domain: Domain for which the cookie is valid. secure: Https is required for the cookie. httponly: Forbids javascript to access the cookie via ``document.cookie``. samesite: Controls whether or not a cookie is sent with cross-site requests. Defaults to ``'lax'``. .. seealso:: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie """ is_actionable: ClassVar[Literal[True]] = True value: str # noqa: WPS110
[docs] @classmethod def from_spec(cls, spec: CookieSpec, *, value: str) -> 'NewCookie': # noqa: WPS110 """ Create a cookie with *value* that matches the given *spec*. Use it when the cookie is described by ``@validate``, but its value is only known in runtime. Copying the flags by hand would mean two places to keep in sync, and a response cookie that does not match its spec is a validation error. .. versionadded:: 0.15.0 """ return cls( value=value, path=spec.path, max_age=spec.max_age, expires=spec.expires, domain=spec.domain, secure=spec.secure, httponly=spec.httponly, samesite=spec.samesite, )
[docs] def to_spec(self) -> CookieSpec: """Converts the modification to spec.""" return CookieSpec( path=self.path, max_age=self.max_age, expires=self.expires, domain=self.domain, secure=self.secure, httponly=self.httponly, samesite=self.samesite, )
[docs] def set_cookies( response: HttpResponseBase, cookies: Mapping[str, NewCookie], ) -> None: """Set cookies for the HTTP response.""" for cookie_key, cookie in cookies.items(): # TODO: fix stubs for `set_cookie` in `django-stubs` response.set_cookie( cookie_key, path=str(cookie.path), max_age=cookie.max_age, expires=cookie.expires, # type: ignore[arg-type] domain=cookie.domain, secure=cookie.secure, httponly=cookie.httponly, samesite=cookie.samesite, # type: ignore[arg-type] value=cookie.value, )