from __future__ import annotations import collections.abc as cabc import typing as t from inspect import cleandoc from .mixins import ImmutableDictMixin from .structures import CallbackDict def cache_control_property( key: str, empty: t.Any, type: type[t.Any] | None, *, doc: str | None = None ) -> t.Any: """Return a new property object for a cache header. Useful if you want to add support for a cache extension in a subclass. :param key: The attribute name present in the parsed cache-control header dict. :param empty: The value to use if the key is present without a value. :param type: The type to convert the string value to instead of a string. If conversion raises a ``ValueError``, the returned value is ``None``. :param doc: The docstring for the property. If not given, it is generated based on the other params. .. versionchanged:: 3.1 Added the ``doc`` param. .. versionchanged:: 2.0 Renamed from ``cache_property``. """ if doc is None: parts = [f"The ``{key}`` attribute."] if type is bool: parts.append("A ``bool``, either present or not.") else: if type is None: parts.append("A ``str``,") else: parts.append(f"A ``{type.__name__}``,") if empty is not None: parts.append(f"``{empty!r}`` if present with no value,") parts.append("or ``None`` if not present.") doc = " ".join(parts) return property( lambda x: x._get_cache_value(key, empty, type), lambda x, v: x._set_cache_value(key, v, type), lambda x: x._del_cache_value(key), doc=cleandoc(doc), ) class _CacheControl(CallbackDict[str, t.Optional[str]]): """Subclass of a dict that stores values for a Cache-Control header. It has accessors for all the cache-control directives specified in RFC 2616. The class does not differentiate between request and response directives. Because the cache-control directives in the HTTP header use dashes the python descriptors use underscores for that. To get a header of the :class:`CacheControl` object again you can convert the object into a string or call the :meth:`to_header` method. If you plan to subclass it and add your own items have a look at the sourcecode for that class. .. versionchanged:: 3.1 Dict values are always ``str | None``. Setting properties will convert the value to a string. Setting a non-bool property to ``False`` is equivalent to setting it to ``None``. Getting typed properties will return ``None`` if conversion raises ``ValueError``, rather than the string. .. versionchanged:: 2.1 Setting int properties such as ``max_age`` will convert the value to an int. .. versionchanged:: 0.4 Setting ``no_cache`` or ``private`` to ``True`` will set the implicit value ``"*"``. """ no_store: bool = cache_control_property("no-store", None, bool) max_age: int | None = cache_control_property("max-age", None, int) no_transform: bool = cache_control_property("no-transform", None, bool) stale_if_error: int | None = cache_control_property("stale-if-error", None, int) def __init__( self, values: cabc.Mapping[str, t.Any] | cabc.Iterable[tuple[str, t.Any]] | None = (), on_update: cabc.Callable[[_CacheControl], None] | None = None, ): super().__init__(values, on_update) self.provided = values is not None def _get_cache_value( self, key: str, empty: t.Any, type: type[t.Any] | None ) -> t.Any: """Used internally by the accessor properties.""" if type is bool: return key in self if key not in self: return None if (value := self[key]) is None: return empty if type is not None: try: value = type(value) except ValueError: return None return value def _set_cache_value( self, key: str, value: t.Any, type: type[t.Any] | None ) -> None: """Used internally by the accessor properties.""" if type is bool: if value: self[key] = None else: self.pop(key, None) elif value is None or value is False: self.pop(key, None) elif value is True: self[key] = None else: if type is not None: value = type(value) self[key] = str(value) def _del_cache_value(self, key: str) -> None: """Used internally by the accessor properties.""" if key in self: del self[key] def to_header(self) -> str: """Convert the stored values into a cache control header.""" return http.dump_header(self) def __str__(self) -> str: return self.to_header() def __repr__(self) -> str: kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) return f"<{type(self).__name__} {kv_str}>" cache_property = staticmethod(cache_control_property) class RequestCacheControl(ImmutableDictMixin[str, t.Optional[str]], _CacheControl): # type: ignore[misc] """A cache control for requests. This is immutable and gives access to all the request-relevant cache control headers. To get a header of the :class:`RequestCacheControl` object again you can convert the object into a string or call the :meth:`to_header` method. If you plan to subclass it and add your own items have a look at the sourcecode for that class. .. versionchanged:: 3.1 Dict values are always ``str | None``. Setting properties will convert the value to a string. Setting a non-bool property to ``False`` is equivalent to setting it to ``None``. Getting typed properties will return ``None`` if conversion raises ``ValueError``, rather than the string. .. versionchanged:: 3.1 ``max_age`` is ``None`` if present without a value, rather than ``-1``. .. versionchanged:: 3.1 ``no_cache`` is a boolean, it is ``True`` instead of ``"*"`` when present. .. versionchanged:: 3.1 ``max_stale`` is ``True`` if present without a value, rather than ``"*"``. .. versionchanged:: 3.1 ``no_transform`` is a boolean. Previously it was mistakenly always ``None``. .. versionchanged:: 3.1 ``min_fresh`` is ``None`` if present without a value, rather than ``"*"``. .. versionchanged:: 2.1 Setting int properties such as ``max_age`` will convert the value to an int. .. versionadded:: 0.5 Response-only properties are not present on this request class. """ no_cache: bool = cache_control_property("no-cache", None, bool) max_stale: int | t.Literal[True] | None = cache_control_property( "max-stale", True, int, ) min_fresh: int | None = cache_control_property("min-fresh", None, int) only_if_cached: bool = cache_control_property("only-if-cached", None, bool) class ResponseCacheControl(_CacheControl): """A cache control for responses. Unlike :class:`RequestCacheControl` this is mutable and gives access to response-relevant cache control headers. To get a header of the :class:`ResponseCacheControl` object again you can convert the object into a string or call the :meth:`to_header` method. If you plan to subclass it and add your own items have a look at the sourcecode for that class. .. versionchanged:: 3.1 Dict values are always ``str | None``. Setting properties will convert the value to a string. Setting a non-bool property to ``False`` is equivalent to setting it to ``None``. Getting typed properties will return ``None`` if conversion raises ``ValueError``, rather than the string. .. versionchanged:: 3.1 ``no_cache`` is ``True`` if present without a value, rather than ``"*"``. .. versionchanged:: 3.1 ``private`` is ``True`` if present without a value, rather than ``"*"``. .. versionchanged:: 3.1 ``no_transform`` is a boolean. Previously it was mistakenly always ``None``. .. versionchanged:: 3.1 Added the ``must_understand``, ``stale_while_revalidate``, and ``stale_if_error`` properties. .. versionchanged:: 2.1.1 ``s_maxage`` converts the value to an int. .. versionchanged:: 2.1 Setting int properties such as ``max_age`` will convert the value to an int. .. versionadded:: 0.5 Request-only properties are not present on this response class. """ no_cache: str | t.Literal[True] | None = cache_control_property( "no-cache", True, None ) public: bool = cache_control_property("public", None, bool) private: str | t.Literal[True] | None = cache_control_property( "private", True, None ) must_revalidate: bool = cache_control_property("must-revalidate", None, bool) proxy_revalidate: bool = cache_control_property("proxy-revalidate", None, bool) s_maxage: int | None = cache_control_property("s-maxage", None, int) immutable: bool = cache_control_property("immutable", None, bool) must_understand: bool = cache_control_property("must-understand", None, bool) stale_while_revalidate: int | None = cache_control_property( "stale-while-revalidate", None, int ) # circular dependencies from .. import http