1"""Action definitions.
2
3:copyright: (c) 2024 Tanner Corcoran
4:license: Apache 2.0, see LICENSE for more details.
5
6"""
7
8import dataclasses
9import sys
10from collections.abc import Generator, Mapping
11from types import MappingProxyType
12from typing import Annotated, Any, Union, final, get_args, get_origin
13
14if sys.version_info >= (3, 10): # pragma: no cover
15 from typing import TypeAlias
16else: # pragma: no cover
17 from typing_extensions import TypeAlias
18# not 3.11 because we need frozen_default
19if sys.version_info >= (3, 12): # pragma: no cover
20 from typing import dataclass_transform
21else: # pragma: no cover
22 from typing_extensions import dataclass_transform
23
24from .__version__ import * # noqa: F401,F403
25from ._internals import WrappingDataclass
26from .enums import HTTPMethod
27
28__all__ = (
29 "ViewAction",
30 "BroadcastAction",
31 "HTTPAction",
32 "ReceivedViewAction",
33 "ReceivedBroadcastAction",
34 "ReceivedHTTPAction",
35 "ReceivedAction",
36)
37
38
[docs]
39@dataclass_transform(frozen_default=True)
40class _Action:
41 """Action base class that is used to handle formatting."""
42
43 _action: str
44 _context: MappingProxyType[str, bool]
45
46 def __init_subclass__(cls, action: str) -> None:
47 """Handle subclass arguments, initialize dataclass, and build
48 the serialization context.
49
50 :param action: The name of the action.
51 :type action: str
52
53 """
54 cls._action = action
55 cls._context = MappingProxyType(
56 {k: cls._get_context(a) for k, a in cls.__annotations__.items()}
57 )
58 dataclasses.dataclass(frozen=True)(cls)
59
60 @final
61 @classmethod
62 def _get_context(cls, annotation: type) -> bool:
63 """Get the context information from the given type annotation.
64
65 :param annotation: The annotation to get the context from.
66 :type annotation: type
67 :return: Whether or not this annotation should be considered an
68 assignment. That is, whether it should be formatted as
69 ``k.x=y`` as opposed to ``x=y``.
70 :rtype: bool
71
72 """
73 origin = get_origin(annotation)
74
75 if origin is not Annotated:
76 return True
77
78 # typing.Annotated must have at least two arguments, so we know
79 # that index 1 will not be out of range
80 return get_args(annotation)[1]
81
82 @final
83 @classmethod
84 def _default_serializer(cls, key: Union[str, None], value: Any) -> str:
85 """Serializer used to serialize values.
86
87 Does the following:
88
89 - Escapes backslashes (``\\``) and double quotes (``"``) in
90 strings
91
92 - Encloses strings in double quotes if needed
93
94 - Formats booleans as strings
95
96 - Formats mappings as ``<key>.k=v,...``
97 (e.g. ``extras.x=y``)
98
99 :param key: A key that must be provided if the value is a
100 mapping.
101 :type key: str | None
102 :param value: The value to serialize.
103 :type value: typing.Any
104
105 :return: The serialized value.
106 :rtype: str
107
108 """
109 if isinstance(value, str):
110 for k in ("\\", '"'):
111 value = value.replace(k, f"\\{k}")
112 if set(value).intersection({",", ";", "=", "'", '"', "\\"}):
113 return f'"{value}"'
114 return value
115
116 if isinstance(value, bool):
117 return ("false", "true")[value]
118
119 if key and isinstance(value, Mapping):
120 return ",".join(
121 f"{key}.{k}={cls._default_serializer(None, v)}"
122 for k, v in value.items()
123 )
124
125 raise TypeError(f"Unknown type: {value.__class__.__name__!r}")
126
127 @final
128 def _serialize(self) -> Generator[str, None, None]:
129 """Generate segments that will later be joined in
130 :meth:`.serialize`.
131
132 """
133 yield self._action
134
135 for k, v in self.__dict__.items():
136 if v is None:
137 continue
138
139 assign = self._context[k]
140 value = self._default_serializer(k, v)
141
142 if not assign:
143 yield value
144 continue
145
146 yield f"{k}={value}"
147
[docs]
148 @final
149 def serialize(self) -> str:
150 """Serialize this action into a single header value.
151
152 :returns: The serialized header value.
153 :rtype: str
154
155 """
156 return ",".join(self._serialize())
157
158
[docs]
159class ViewAction(_Action, action="view"):
160 """A serializable view action.
161
162 :param label: The action label.
163 :type label: str
164 :param url: The URL to open when the action is activated.
165 :type url: str
166 :param clear: Whether or not to clear the notification after the
167 action is activated.
168 :type clear: bool | None, optional
169
170 """
171
172 label: Annotated[str, False]
173 """See the :paramref:`~ViewAction.label` parameter."""
174
175 url: Annotated[str, False]
176 """See the :paramref:`~ViewAction.url` parameter."""
177
178 clear: Union[bool, None] = None
179 """See the :paramref:`~ViewAction.clear` parameter."""
180
181
[docs]
182class BroadcastAction(_Action, action="broadcast"):
183 """A serializable broadcast action.
184
185 :param label: The action label.
186 :type label: str
187 :param intent: The Android intent name.
188 :type intent: str | None, optional
189 :param extras: The Android intent extras.
190 :type extras: typing.Mapping[str, str] | None, optional
191 :param clear: Whether or not to clear the notification after the
192 action is activated.
193 :type clear: bool | None, optional
194
195 """
196
197 label: Annotated[str, False]
198 """See the :paramref:`~BroadcastAction.label` parameter."""
199
200 intent: Union[str, None] = None
201 """See the :paramref:`~BroadcastAction.intent` parameter."""
202
203 extras: Annotated[Union[Mapping[str, str], None], False] = None
204 """See the :paramref:`~BroadcastAction.extras` parameter."""
205
206 clear: Union[bool, None] = None
207 """See the :paramref:`~BroadcastAction.clear` parameter."""
208
209
[docs]
210class HTTPAction(_Action, action="http"):
211 """A serializable HTTP action.
212
213 :param label: The action label.
214 :type label: str
215 :param url: The URL to send the HTTP request to.
216 :type url: str
217 :param method: The HTTP method.
218 :type method: str | None, optional
219 :param headers: The HTTP headers.
220 :type headers: typing.Mapping[str, str] | None, optional
221 :param body: The HTTP body.
222 :type body: str | None, optional
223 :param clear: Whether or not to clear the notification after the
224 action is activated.
225 :type clear: bool | None, optional
226
227 """
228
229 label: Annotated[str, False]
230 """See the :paramref:`~HTTPAction.label` parameter."""
231
232 url: Annotated[str, False]
233 """See the :paramref:`~HTTPAction.url` parameter."""
234
235 method: Union[str, None] = None
236 """See the :paramref:`~HTTPAction.method` parameter."""
237
238 headers: Annotated[Union[Mapping[str, str], None], False] = None
239 """See the :paramref:`~HTTPAction.headers` parameter."""
240
241 body: Union[str, None] = None
242 """See the :paramref:`~HTTPAction.body` parameter."""
243
244 clear: Union[bool, None] = None
245 """See the :paramref:`~HTTPAction.clear` parameter."""
246
247
[docs]
248class ReceivedViewAction(WrappingDataclass):
249 """A received view action. Similar to :class:`ViewAction`, but
250 cannot be serialized.
251
252 :param id: The action ID.
253 :type id: str
254 :param label: The action label.
255 :type label: str
256 :param url: The URL to open when the action is activated.
257 :type url: str
258 :param clear: Whether or not to clear the notification after the
259 action is activated.
260 :type clear: bool
261
262 """
263
264 id: str
265 """See the :paramref:`~ReceivedViewAction.id` parameter."""
266
267 label: str
268 """See the :paramref:`~ReceivedViewAction.label` parameter."""
269
270 url: str
271 """See the :paramref:`~ReceivedViewAction.url` parameter."""
272
273 clear: bool
274 """See the :paramref:`~ReceivedViewAction.clear` parameter."""
275
276
[docs]
277class ReceivedBroadcastAction(WrappingDataclass):
278 """A received broadcast action. Similar to :class:`BroadcastAction`,
279 but cannot be serialized.
280
281 :param id: The action ID.
282 :type id: str
283 :param label: The action label.
284 :type label: str
285 :param clear: Whether or not to clear the notification after the
286 action is activated.
287 :type clear: bool
288 :param intent: The Android intent name.
289 :type intent: str | None, optional
290 :param extras: The Android intent extras.
291 :type extras: dict[str, str] | None, optional
292
293 """
294
295 id: str
296 """See the :paramref:`~ReceivedBroadcastAction.id` parameter."""
297
298 label: str
299 """See the :paramref:`~ReceivedBroadcastAction.label` parameter."""
300
301 clear: bool
302 """See the :paramref:`~ReceivedBroadcastAction.clear` parameter."""
303
304 intent: Union[str, None] = None
305 """See the :paramref:`~ReceivedBroadcastAction.intent` parameter."""
306
307 extras: Union[dict[str, str], None] = None
308 """See the :paramref:`~ReceivedBroadcastAction.extras` parameter."""
309
310
[docs]
311class ReceivedHTTPAction(WrappingDataclass):
312 """A received HTTP action. Similar to :class:`HTTPAction`,
313 but cannot be serialized.
314
315 :param id: The action ID.
316 :type id: str
317 :param label: The action label.
318 :type label: str
319 :param url: The URL to send the HTTP request to.
320 :type url: str
321 :param clear: Whether or not to clear the notification after the
322 action is activated.
323 :type clear: bool
324 :param method: The HTTP method.
325 :type method: HTTPMethod | None, optional
326 :param headers: The HTTP headers.
327 :type headers: dict[str, str] | None, optional
328 :param body: The HTTP body.
329 :type body: str | None, optional
330
331 """
332
333 id: str
334 """See the :paramref:`~ReceivedHTTPAction.id` parameter."""
335
336 label: str
337 """See the :paramref:`~ReceivedHTTPAction.label` parameter."""
338
339 url: str
340 """See the :paramref:`~ReceivedHTTPAction.url` parameter."""
341
342 clear: bool
343 """See the :paramref:`~ReceivedHTTPAction.clear` parameter."""
344
345 method: Annotated[Union[HTTPMethod, None], HTTPMethod] = None
346 """See the :paramref:`~ReceivedHTTPAction.method` parameter."""
347
348 headers: Union[dict[str, str], None] = None
349 """See the :paramref:`~ReceivedHTTPAction.headers` parameter."""
350
351 body: Union[str, None] = None
352 """See the :paramref:`~ReceivedHTTPAction.body` parameter."""
353
354
355ReceivedAction: TypeAlias = Union[
356 ReceivedViewAction, ReceivedBroadcastAction, ReceivedHTTPAction
357]