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