cache_control.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. from __future__ import annotations
  2. import collections.abc as cabc
  3. import typing as t
  4. from inspect import cleandoc
  5. from .mixins import ImmutableDictMixin
  6. from .structures import CallbackDict
  7. def cache_control_property(
  8. key: str, empty: t.Any, type: type[t.Any] | None, *, doc: str | None = None
  9. ) -> t.Any:
  10. """Return a new property object for a cache header. Useful if you
  11. want to add support for a cache extension in a subclass.
  12. :param key: The attribute name present in the parsed cache-control header dict.
  13. :param empty: The value to use if the key is present without a value.
  14. :param type: The type to convert the string value to instead of a string. If
  15. conversion raises a ``ValueError``, the returned value is ``None``.
  16. :param doc: The docstring for the property. If not given, it is generated
  17. based on the other params.
  18. .. versionchanged:: 3.1
  19. Added the ``doc`` param.
  20. .. versionchanged:: 2.0
  21. Renamed from ``cache_property``.
  22. """
  23. if doc is None:
  24. parts = [f"The ``{key}`` attribute."]
  25. if type is bool:
  26. parts.append("A ``bool``, either present or not.")
  27. else:
  28. if type is None:
  29. parts.append("A ``str``,")
  30. else:
  31. parts.append(f"A ``{type.__name__}``,")
  32. if empty is not None:
  33. parts.append(f"``{empty!r}`` if present with no value,")
  34. parts.append("or ``None`` if not present.")
  35. doc = " ".join(parts)
  36. return property(
  37. lambda x: x._get_cache_value(key, empty, type),
  38. lambda x, v: x._set_cache_value(key, v, type),
  39. lambda x: x._del_cache_value(key),
  40. doc=cleandoc(doc),
  41. )
  42. class _CacheControl(CallbackDict[str, t.Optional[str]]):
  43. """Subclass of a dict that stores values for a Cache-Control header. It
  44. has accessors for all the cache-control directives specified in RFC 2616.
  45. The class does not differentiate between request and response directives.
  46. Because the cache-control directives in the HTTP header use dashes the
  47. python descriptors use underscores for that.
  48. To get a header of the :class:`CacheControl` object again you can convert
  49. the object into a string or call the :meth:`to_header` method. If you plan
  50. to subclass it and add your own items have a look at the sourcecode for
  51. that class.
  52. .. versionchanged:: 3.1
  53. Dict values are always ``str | None``. Setting properties will
  54. convert the value to a string. Setting a non-bool property to
  55. ``False`` is equivalent to setting it to ``None``. Getting typed
  56. properties will return ``None`` if conversion raises
  57. ``ValueError``, rather than the string.
  58. .. versionchanged:: 2.1
  59. Setting int properties such as ``max_age`` will convert the
  60. value to an int.
  61. .. versionchanged:: 0.4
  62. Setting ``no_cache`` or ``private`` to ``True`` will set the
  63. implicit value ``"*"``.
  64. """
  65. no_store: bool = cache_control_property("no-store", None, bool)
  66. max_age: int | None = cache_control_property("max-age", None, int)
  67. no_transform: bool = cache_control_property("no-transform", None, bool)
  68. stale_if_error: int | None = cache_control_property("stale-if-error", None, int)
  69. def __init__(
  70. self,
  71. values: cabc.Mapping[str, t.Any] | cabc.Iterable[tuple[str, t.Any]] | None = (),
  72. on_update: cabc.Callable[[_CacheControl], None] | None = None,
  73. ):
  74. super().__init__(values, on_update)
  75. self.provided = values is not None
  76. def _get_cache_value(
  77. self, key: str, empty: t.Any, type: type[t.Any] | None
  78. ) -> t.Any:
  79. """Used internally by the accessor properties."""
  80. if type is bool:
  81. return key in self
  82. if key not in self:
  83. return None
  84. if (value := self[key]) is None:
  85. return empty
  86. if type is not None:
  87. try:
  88. value = type(value)
  89. except ValueError:
  90. return None
  91. return value
  92. def _set_cache_value(
  93. self, key: str, value: t.Any, type: type[t.Any] | None
  94. ) -> None:
  95. """Used internally by the accessor properties."""
  96. if type is bool:
  97. if value:
  98. self[key] = None
  99. else:
  100. self.pop(key, None)
  101. elif value is None or value is False:
  102. self.pop(key, None)
  103. elif value is True:
  104. self[key] = None
  105. else:
  106. if type is not None:
  107. value = type(value)
  108. self[key] = str(value)
  109. def _del_cache_value(self, key: str) -> None:
  110. """Used internally by the accessor properties."""
  111. if key in self:
  112. del self[key]
  113. def to_header(self) -> str:
  114. """Convert the stored values into a cache control header."""
  115. return http.dump_header(self)
  116. def __str__(self) -> str:
  117. return self.to_header()
  118. def __repr__(self) -> str:
  119. kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items()))
  120. return f"<{type(self).__name__} {kv_str}>"
  121. cache_property = staticmethod(cache_control_property)
  122. class RequestCacheControl(ImmutableDictMixin[str, t.Optional[str]], _CacheControl): # type: ignore[misc]
  123. """A cache control for requests. This is immutable and gives access
  124. to all the request-relevant cache control headers.
  125. To get a header of the :class:`RequestCacheControl` object again you can
  126. convert the object into a string or call the :meth:`to_header` method. If
  127. you plan to subclass it and add your own items have a look at the sourcecode
  128. for that class.
  129. .. versionchanged:: 3.1
  130. Dict values are always ``str | None``. Setting properties will
  131. convert the value to a string. Setting a non-bool property to
  132. ``False`` is equivalent to setting it to ``None``. Getting typed
  133. properties will return ``None`` if conversion raises
  134. ``ValueError``, rather than the string.
  135. .. versionchanged:: 3.1
  136. ``max_age`` is ``None`` if present without a value, rather
  137. than ``-1``.
  138. .. versionchanged:: 3.1
  139. ``no_cache`` is a boolean, it is ``True`` instead of ``"*"``
  140. when present.
  141. .. versionchanged:: 3.1
  142. ``max_stale`` is ``True`` if present without a value, rather
  143. than ``"*"``.
  144. .. versionchanged:: 3.1
  145. ``no_transform`` is a boolean. Previously it was mistakenly
  146. always ``None``.
  147. .. versionchanged:: 3.1
  148. ``min_fresh`` is ``None`` if present without a value, rather
  149. than ``"*"``.
  150. .. versionchanged:: 2.1
  151. Setting int properties such as ``max_age`` will convert the
  152. value to an int.
  153. .. versionadded:: 0.5
  154. Response-only properties are not present on this request class.
  155. """
  156. no_cache: bool = cache_control_property("no-cache", None, bool)
  157. max_stale: int | t.Literal[True] | None = cache_control_property(
  158. "max-stale",
  159. True,
  160. int,
  161. )
  162. min_fresh: int | None = cache_control_property("min-fresh", None, int)
  163. only_if_cached: bool = cache_control_property("only-if-cached", None, bool)
  164. class ResponseCacheControl(_CacheControl):
  165. """A cache control for responses. Unlike :class:`RequestCacheControl`
  166. this is mutable and gives access to response-relevant cache control
  167. headers.
  168. To get a header of the :class:`ResponseCacheControl` object again you can
  169. convert the object into a string or call the :meth:`to_header` method. If
  170. you plan to subclass it and add your own items have a look at the sourcecode
  171. for that class.
  172. .. versionchanged:: 3.1
  173. Dict values are always ``str | None``. Setting properties will
  174. convert the value to a string. Setting a non-bool property to
  175. ``False`` is equivalent to setting it to ``None``. Getting typed
  176. properties will return ``None`` if conversion raises
  177. ``ValueError``, rather than the string.
  178. .. versionchanged:: 3.1
  179. ``no_cache`` is ``True`` if present without a value, rather than
  180. ``"*"``.
  181. .. versionchanged:: 3.1
  182. ``private`` is ``True`` if present without a value, rather than
  183. ``"*"``.
  184. .. versionchanged:: 3.1
  185. ``no_transform`` is a boolean. Previously it was mistakenly
  186. always ``None``.
  187. .. versionchanged:: 3.1
  188. Added the ``must_understand``, ``stale_while_revalidate``, and
  189. ``stale_if_error`` properties.
  190. .. versionchanged:: 2.1.1
  191. ``s_maxage`` converts the value to an int.
  192. .. versionchanged:: 2.1
  193. Setting int properties such as ``max_age`` will convert the
  194. value to an int.
  195. .. versionadded:: 0.5
  196. Request-only properties are not present on this response class.
  197. """
  198. no_cache: str | t.Literal[True] | None = cache_control_property(
  199. "no-cache", True, None
  200. )
  201. public: bool = cache_control_property("public", None, bool)
  202. private: str | t.Literal[True] | None = cache_control_property(
  203. "private", True, None
  204. )
  205. must_revalidate: bool = cache_control_property("must-revalidate", None, bool)
  206. proxy_revalidate: bool = cache_control_property("proxy-revalidate", None, bool)
  207. s_maxage: int | None = cache_control_property("s-maxage", None, int)
  208. immutable: bool = cache_control_property("immutable", None, bool)
  209. must_understand: bool = cache_control_property("must-understand", None, bool)
  210. stale_while_revalidate: int | None = cache_control_property(
  211. "stale-while-revalidate", None, int
  212. )
  213. # circular dependencies
  214. from .. import http