codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import collections.abc as cabc4import typing as t5from inspect import cleandoc6 7from .mixins import ImmutableDictMixin8from .structures import CallbackDict9 10 11def cache_control_property(12 key: str, empty: t.Any, type: type[t.Any] | None, *, doc: str | None = None13) -> t.Any:14 """Return a new property object for a cache header. Useful if you15 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. If20 conversion raises a ``ValueError``, the returned value is ``None``.21 :param doc: The docstring for the property. If not given, it is generated22 based on the other params.23 24 .. versionchanged:: 3.125 Added the ``doc`` param.26 27 .. versionchanged:: 2.028 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. It58 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 the62 python descriptors use underscores for that.63 64 To get a header of the :class:`CacheControl` object again you can convert65 the object into a string or call the :meth:`to_header` method. If you plan66 to subclass it and add your own items have a look at the sourcecode for67 that class.68 69 .. versionchanged:: 3.170 Dict values are always ``str | None``. Setting properties will71 convert the value to a string. Setting a non-bool property to72 ``False`` is equivalent to setting it to ``None``. Getting typed73 properties will return ``None`` if conversion raises74 ``ValueError``, rather than the string.75 76 .. versionchanged:: 2.177 Setting int properties such as ``max_age`` will convert the78 value to an int.79 80 .. versionchanged:: 0.481 Setting ``no_cache`` or ``private`` to ``True`` will set the82 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 None97 98 def _get_cache_value(99 self, key: str, empty: t.Any, type: type[t.Any] | None100 ) -> t.Any:101 """Used internally by the accessor properties."""102 if type is bool:103 return key in self104 105 if key not in self:106 return None107 108 if (value := self[key]) is None:109 return empty110 111 if type is not None:112 try:113 value = type(value)114 except ValueError:115 return None116 117 return value118 119 def _set_cache_value(120 self, key: str, value: t.Any, type: type[t.Any] | None121 ) -> None:122 """Used internally by the accessor properties."""123 if type is bool:124 if value:125 self[key] = None126 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] = None132 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 access159 to all the request-relevant cache control headers.160 161 To get a header of the :class:`RequestCacheControl` object again you can162 convert the object into a string or call the :meth:`to_header` method. If163 you plan to subclass it and add your own items have a look at the sourcecode164 for that class.165 166 .. versionchanged:: 3.1167 Dict values are always ``str | None``. Setting properties will168 convert the value to a string. Setting a non-bool property to169 ``False`` is equivalent to setting it to ``None``. Getting typed170 properties will return ``None`` if conversion raises171 ``ValueError``, rather than the string.172 173 .. versionchanged:: 3.1174 ``max_age`` is ``None`` if present without a value, rather175 than ``-1``.176 177 .. versionchanged:: 3.1178 ``no_cache`` is a boolean, it is ``True`` instead of ``"*"``179 when present.180 181 .. versionchanged:: 3.1182 ``max_stale`` is ``True`` if present without a value, rather183 than ``"*"``.184 185 .. versionchanged:: 3.1186 ``no_transform`` is a boolean. Previously it was mistakenly187 always ``None``.188 189 .. versionchanged:: 3.1190 ``min_fresh`` is ``None`` if present without a value, rather191 than ``"*"``.192 193 .. versionchanged:: 2.1194 Setting int properties such as ``max_age`` will convert the195 value to an int.196 197 .. versionadded:: 0.5198 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 control214 headers.215 216 To get a header of the :class:`ResponseCacheControl` object again you can217 convert the object into a string or call the :meth:`to_header` method. If218 you plan to subclass it and add your own items have a look at the sourcecode219 for that class.220 221 .. versionchanged:: 3.1222 Dict values are always ``str | None``. Setting properties will223 convert the value to a string. Setting a non-bool property to224 ``False`` is equivalent to setting it to ``None``. Getting typed225 properties will return ``None`` if conversion raises226 ``ValueError``, rather than the string.227 228 .. versionchanged:: 3.1229 ``no_cache`` is ``True`` if present without a value, rather than230 ``"*"``.231 232 .. versionchanged:: 3.1233 ``private`` is ``True`` if present without a value, rather than234 ``"*"``.235 236 .. versionchanged:: 3.1237 ``no_transform`` is a boolean. Previously it was mistakenly238 always ``None``.239 240 .. versionchanged:: 3.1241 Added the ``must_understand``, ``stale_while_revalidate``, and242 ``stale_if_error`` properties.243 244 .. versionchanged:: 2.1.1245 ``s_maxage`` converts the value to an int.246 247 .. versionchanged:: 2.1248 Setting int properties such as ``max_age`` will convert the249 value to an int.250 251 .. versionadded:: 0.5252 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, None257 )258 public: bool = cache_control_property("public", None, bool)259 private: str | t.Literal[True] | None = cache_control_property(260 "private", True, None261 )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, int269 )270 271 272# circular dependencies273from .. import http # noqa: E402274 