Source code for ntfy_api.actions

  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]