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