quickmacapp 2025.3.18__py3-none-any.whl → 2025.4.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,498 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from types import TracebackType
5
+ from typing import Any, Awaitable, Callable, Protocol, TypeAlias
6
+ from zoneinfo import ZoneInfo
7
+
8
+ from datetype import DateTime
9
+ from Foundation import (
10
+ NSTimeZone,
11
+ NSDateComponents,
12
+ NSError,
13
+ NSLog,
14
+ NSObject,
15
+ NSCalendar,
16
+ NSCalendarIdentifierGregorian,
17
+ )
18
+ from objc import object_property
19
+ from twisted.internet.defer import Deferred
20
+ from UserNotifications import (
21
+ UNAuthorizationOptionNone,
22
+ UNCalendarNotificationTrigger,
23
+ UNMutableNotificationContent,
24
+ UNNotification,
25
+ UNNotificationAction,
26
+ UNNotificationActionOptionAuthenticationRequired,
27
+ UNNotificationActionOptionDestructive,
28
+ UNNotificationActionOptionForeground,
29
+ UNNotificationActionOptions,
30
+ UNNotificationCategory,
31
+ UNNotificationCategoryOptionAllowInCarPlay,
32
+ UNNotificationCategoryOptionHiddenPreviewsShowSubtitle,
33
+ UNNotificationCategoryOptionHiddenPreviewsShowTitle,
34
+ UNNotificationCategoryOptions,
35
+ UNNotificationPresentationOptionBanner,
36
+ UNNotificationPresentationOptions,
37
+ UNNotificationRequest,
38
+ UNNotificationResponse,
39
+ UNNotificationSettings,
40
+ UNNotificationTrigger,
41
+ UNTextInputNotificationAction,
42
+ UNUserNotificationCenter,
43
+ )
44
+
45
+
46
+ def make[T: NSObject](cls: type[T], **attributes: object) -> T:
47
+ self: T = cls.alloc().init()
48
+ self.setValuesForKeysWithDictionary_(attributes)
49
+ return self
50
+
51
+
52
+ @dataclass
53
+ class _AppNotificationsCtxBuilder:
54
+ _center: UNUserNotificationCenter
55
+ _cfg: _NotifConfigImpl | None
56
+
57
+ async def __aenter__(self) -> _NotifConfigImpl:
58
+ """
59
+ Request authorization, then start building this notifications manager.
60
+ """
61
+ NSLog("beginning build")
62
+ grantDeferred: Deferred[bool] = Deferred()
63
+
64
+ def completed(granted: bool, error: NSError | None) -> None:
65
+ # TODO: convert non-None NSErrors into failures on this Deferred
66
+ grantDeferred.callback(granted)
67
+ NSLog(
68
+ "Notification authorization response: %@ with error: %@", granted, error
69
+ )
70
+
71
+ NSLog("requesting authorization")
72
+ self._center.requestAuthorizationWithOptions_completionHandler_(
73
+ UNAuthorizationOptionNone, completed
74
+ )
75
+ NSLog("requested")
76
+ granted = await grantDeferred
77
+ settingsDeferred: Deferred[UNNotificationSettings] = Deferred()
78
+
79
+ def gotSettings(settings: UNNotificationSettings) -> None:
80
+ NSLog("received notification settings %@", settings)
81
+ settingsDeferred.callback(settings)
82
+
83
+ NSLog("requesting notification settings")
84
+ self._center.getNotificationSettingsWithCompletionHandler_(gotSettings)
85
+ settings = await settingsDeferred
86
+ NSLog("initializing config")
87
+ self.cfg = _NotifConfigImpl(
88
+ self._center,
89
+ [],
90
+ _wasGrantedPermission=granted,
91
+ _settings=settings,
92
+ )
93
+ NSLog("done!")
94
+ return self.cfg
95
+
96
+ async def __aexit__(
97
+ self,
98
+ exc_type: type[BaseException] | None,
99
+ exc_value: BaseException | None,
100
+ traceback: TracebackType | None,
101
+ /,
102
+ ) -> bool:
103
+ """
104
+ Finalize the set of notification categories and actions in use for this application.
105
+ """
106
+ NSLog("async exit from ctx manager")
107
+ if traceback is None and self.cfg is not None:
108
+ qmandw = _QMANotificationDelegateWrapper.alloc().initWithConfig_(self.cfg)
109
+ qmandw.retain()
110
+ NSLog("Setting delegate! %@", qmandw)
111
+ self._center.setDelegate_(qmandw)
112
+ self.cfg._register()
113
+ else:
114
+ NSLog("NOT setting delegate!!!")
115
+ return False
116
+
117
+
118
+ class _QMANotificationDelegateWrapper(NSObject):
119
+ """
120
+ UNUserNotificationCenterDelegate implementation.
121
+ """
122
+
123
+ config: _NotifConfigImpl = object_property()
124
+
125
+ def initWithConfig_(self, cfg: _NotifConfigImpl) -> _QMANotificationDelegateWrapper:
126
+ self.config = cfg
127
+ return self
128
+
129
+ def userNotificationCenter_willPresentNotification_withCompletionHandler_(
130
+ self,
131
+ notificationCenter: UNUserNotificationCenter,
132
+ notification: UNNotification,
133
+ completionHandler: Callable[[UNNotificationPresentationOptions], None],
134
+ ) -> None:
135
+ NSLog("willPresent: %@", notification)
136
+ # TODO: allow for client code to customize this; here we are saying
137
+ # "please present the notification to the user as a banner, even if we
138
+ # are in the foreground". We should allow for customization on a
139
+ # category basis; rather than @response.something, maybe
140
+ # @present.something, as a method on the python category class?
141
+ completionHandler(UNNotificationPresentationOptionBanner)
142
+
143
+ def userNotificationCenter_didReceiveNotificationResponse_withCompletionHandler_(
144
+ self,
145
+ notificationCenter: UNUserNotificationCenter,
146
+ notificationResponse: UNNotificationResponse,
147
+ completionHandler: Callable[[], None],
148
+ ) -> None:
149
+ """
150
+ We received a response to a notification.
151
+ """
152
+ NSLog("received notification repsonse %@", notificationResponse)
153
+
154
+ # TODO: actually hook up the dispatch of the notification response to
155
+ # the registry of action callbacks already set up in
156
+ # NotificationConfig.
157
+ async def respond() -> None:
158
+ notifier = self.config._notifierByCategory(
159
+ notificationResponse.notification()
160
+ .request()
161
+ .content()
162
+ .categoryIdentifier()
163
+ )
164
+ await notifier._handleResponse(notificationResponse)
165
+ completionHandler()
166
+
167
+ Deferred.fromCoroutine(respond()).addErrback(
168
+ lambda error: NSLog("error: %@", error)
169
+ )
170
+
171
+
172
+ class NotificationTranslator[T](Protocol):
173
+ """
174
+ Translate notifications from the notification ID and some user data,
175
+ """
176
+
177
+ def fromNotification(self, notificationID: str, userData: dict[str, Any]) -> T:
178
+ """
179
+ A user interacted with a notification with the given parameters;
180
+ deserialize them into a Python object that can process that action.
181
+ """
182
+
183
+ def toNotification(self, notification: T) -> tuple[str, dict[str, Any]]:
184
+ """
185
+ The application has requested to send a notification to the operating
186
+ system, serialize the Python object represneting this category of
187
+ notification into a 2-tuple of C{notificatcionID}, C{userData} that can
188
+ be encapsulated in a C{UNNotificationRequest}.
189
+ """
190
+
191
+
192
+ @dataclass
193
+ class Notifier[NotifT]:
194
+ """
195
+ A notifier for a specific category.
196
+ """
197
+
198
+ # Public interface:
199
+ def undeliver(self, notification: NotifT) -> None:
200
+ """
201
+ Remove the previously-delivered notification object from the
202
+ notification center, if it's still there.
203
+ """
204
+ notID, _ = self._tx.toNotification(notification)
205
+ self._cfg._center.removeDeliveredNotificationsWithIdentifiers_([notID])
206
+
207
+ def unsend(self, notification: NotifT) -> None:
208
+ """
209
+ Prevent the as-yet undelivered notification object from being
210
+ delivered.
211
+ """
212
+ notID, _ = self._tx.toNotification(notification)
213
+ self._cfg._center.removePendingNotificationRequestsWithIdentifiers_([notID])
214
+
215
+ async def notifyAt(
216
+ self, when: DateTime[ZoneInfo], notification: NotifT, title: str, body: str
217
+ ) -> None:
218
+
219
+ components: NSDateComponents = NSDateComponents.alloc().init()
220
+ components.setCalendar_(
221
+ NSCalendar.calendarWithIdentifier_(NSCalendarIdentifierGregorian)
222
+ )
223
+ components.setTimeZone_(NSTimeZone.timeZoneWithName_(when.tzinfo.key))
224
+ components.setYear_(when.year)
225
+ components.setMonth_(when.month)
226
+ components.setDay_(when.day)
227
+ components.setHour_(when.hour)
228
+ components.setMinute_(when.minute)
229
+ components.setSecond_(when.second)
230
+
231
+ repeats: bool = False
232
+ trigger: UNNotificationTrigger = (
233
+ UNCalendarNotificationTrigger.triggerWithDateMatchingComponents_repeats_(
234
+ components,
235
+ repeats,
236
+ )
237
+ )
238
+ await self._notifyWithTrigger(trigger, notification, title, body)
239
+
240
+ # Attributes:
241
+ _notificationCategoryID: str
242
+ _cfg: _NotifConfigImpl
243
+ _tx: NotificationTranslator[NotifT]
244
+ _actionInfos: list[_oneActionInfo]
245
+ _allowInCarPlay: bool
246
+ _hiddenPreviewsShowTitle: bool
247
+ _hiddenPreviewsShowSubtitle: bool
248
+
249
+ # Private implementation details:
250
+ async def _handleResponse(self, response: UNNotificationResponse) -> None:
251
+ userInfo = response.notification().request().content().userInfo()
252
+ actionID: str = response.actionIdentifier()
253
+ notificationID: str = response.notification().request().identifier()
254
+ cat = self._tx.fromNotification(notificationID, userInfo)
255
+ for cb, eachActionID, action, options in self._actionInfos:
256
+ if actionID == eachActionID:
257
+ break
258
+ else:
259
+ raise KeyError(actionID)
260
+ await cb(cat, response)
261
+
262
+ def _createUNNotificationCategory(self) -> UNNotificationCategory:
263
+ actions = []
264
+ # We don't yet support intent identifiers.
265
+ intentIdentifiers: list[str] = []
266
+ options = 0
267
+ for handler, actionID, toRegister, extraOptions in self._actionInfos:
268
+ options |= extraOptions
269
+ if toRegister is not None:
270
+ actions.append(toRegister)
271
+ NSLog("actions generated: %@ options: %@", actions, options)
272
+ if self._allowInCarPlay:
273
+ # Ha ha. Someday, maybe.
274
+ options |= UNNotificationCategoryOptionAllowInCarPlay
275
+ if self._hiddenPreviewsShowTitle:
276
+ options |= UNNotificationCategoryOptionHiddenPreviewsShowTitle
277
+ if self._hiddenPreviewsShowSubtitle:
278
+ options |= UNNotificationCategoryOptionHiddenPreviewsShowSubtitle
279
+ return UNNotificationCategory.categoryWithIdentifier_actions_intentIdentifiers_options_(
280
+ self._notificationCategoryID, actions, intentIdentifiers, options
281
+ )
282
+
283
+ async def _notifyWithTrigger(
284
+ self,
285
+ trigger: UNNotificationTrigger,
286
+ notification: NotifT,
287
+ title: str,
288
+ body: str,
289
+ ) -> None:
290
+ notificationID, userInfo = self._tx.toNotification(notification)
291
+ request = UNNotificationRequest.requestWithIdentifier_content_trigger_(
292
+ notificationID,
293
+ make(
294
+ UNMutableNotificationContent,
295
+ title=title,
296
+ body=body,
297
+ categoryIdentifier=self._notificationCategoryID,
298
+ userInfo=userInfo,
299
+ ),
300
+ trigger,
301
+ )
302
+ d: Deferred[NSError | None] = Deferred()
303
+ self._cfg._center.addNotificationRequest_withCompletionHandler_(
304
+ request, d.callback
305
+ )
306
+ error = await d
307
+ NSLog("completed notification request with error %@", error)
308
+
309
+
310
+ @dataclass
311
+ class _NotifConfigImpl:
312
+ _center: UNUserNotificationCenter
313
+ _notifiers: list[Notifier[Any]]
314
+ _wasGrantedPermission: bool
315
+ _settings: UNNotificationSettings
316
+
317
+ def add[NotifT](
318
+ self,
319
+ category: type[NotifT],
320
+ translator: NotificationTranslator[NotifT],
321
+ allowInCarPlay: bool = False,
322
+ hiddenPreviewsShowTitle: bool = False,
323
+ hiddenPreviewsShowSubtitle: bool = False,
324
+ # customDismissAction: bool = False,
325
+ ) -> Notifier[NotifT]:
326
+ """
327
+ @param category: the category to add
328
+
329
+ @param translator: a translator that can load and save a translator.
330
+ """
331
+ catid: str = f"{category.__module__}.{category.__qualname__}"
332
+ notifier = Notifier(
333
+ catid,
334
+ self,
335
+ translator,
336
+ _getAllActionInfos(category),
337
+ _allowInCarPlay=allowInCarPlay,
338
+ _hiddenPreviewsShowTitle=hiddenPreviewsShowTitle,
339
+ _hiddenPreviewsShowSubtitle=hiddenPreviewsShowSubtitle,
340
+ )
341
+ self._notifiers.append(notifier)
342
+ return notifier
343
+
344
+ def _notifierByCategory(self, categoryID: str) -> Notifier[Any]:
345
+ for notifier in self._notifiers:
346
+ if categoryID == notifier._notificationCategoryID:
347
+ return notifier
348
+ raise KeyError(categoryID)
349
+
350
+ def _register(self) -> None:
351
+ self._center.setNotificationCategories_(
352
+ [pynot._createUNNotificationCategory() for pynot in self._notifiers]
353
+ )
354
+
355
+
356
+ _ACTION_INFO_ATTR = "__qma_notification_action_info__"
357
+
358
+
359
+ _oneActionInfo = tuple[
360
+ # Action handler to stuff away into dispatch; does the pulling out of
361
+ # userText if necessary
362
+ Callable[[Any, UNNotificationResponse], Awaitable[None]],
363
+ # action ID
364
+ str,
365
+ # the notification action to register; None for default & dismiss
366
+ UNNotificationAction | None,
367
+ UNNotificationCategoryOptions,
368
+ ]
369
+
370
+
371
+ _anyActionInfo: TypeAlias = (
372
+ "_PlainNotificationActionInfo | _TextNotificationActionInfo | _BuiltinActionInfo"
373
+ )
374
+
375
+
376
+ def _getActionInfo(o: object) -> _oneActionInfo | None:
377
+ handler: _anyActionInfo | None = getattr(o, _ACTION_INFO_ATTR, None)
378
+ if handler is None:
379
+ return None
380
+ appCallback: Any = o
381
+ actionID = handler.identifier
382
+ callback = handler._makeCallback(appCallback)
383
+ extraOptions = handler._extraOptions
384
+ return (callback, actionID, handler._toAction(), extraOptions)
385
+
386
+
387
+ def _setActionInfo[T](wrapt: T, actionInfo: _anyActionInfo) -> T:
388
+ setattr(wrapt, _ACTION_INFO_ATTR, actionInfo)
389
+ return wrapt
390
+
391
+
392
+ def _getAllActionInfos(t: type[object]) -> list[_oneActionInfo]:
393
+ result = []
394
+ for attr in dir(t):
395
+ actionInfo = _getActionInfo(getattr(t, attr, None))
396
+ if actionInfo is not None:
397
+ result.append(actionInfo)
398
+ return result
399
+
400
+
401
+ def _py2options(
402
+ foreground: bool,
403
+ destructive: bool,
404
+ authenticationRequired: bool,
405
+ ) -> UNNotificationActionOptions:
406
+ """
407
+ Convert some sensibly-named data types into UNNotificationActionOptions.
408
+ """
409
+ options = 0
410
+ if foreground:
411
+ options |= UNNotificationActionOptionForeground
412
+ if destructive:
413
+ options |= UNNotificationActionOptionDestructive
414
+ if authenticationRequired:
415
+ options |= UNNotificationActionOptionAuthenticationRequired
416
+ return options
417
+
418
+
419
+ @dataclass
420
+ class _PlainNotificationActionInfo:
421
+ identifier: str
422
+ title: str
423
+ foreground: bool
424
+ destructive: bool
425
+ authenticationRequired: bool
426
+ _extraOptions: UNNotificationCategoryOptions = 0
427
+
428
+ def _makeCallback[T](
429
+ self, appCallback: Callable[[T], Awaitable[None]]
430
+ ) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
431
+ async def takesNotification(self: T, response: UNNotificationResponse) -> None:
432
+ await appCallback(self)
433
+ return None
434
+
435
+ return takesNotification
436
+
437
+ def _toAction(self) -> UNNotificationAction:
438
+ return UNNotificationAction.actionWithIdentifier_title_options_(
439
+ self.identifier,
440
+ self.title,
441
+ _py2options(
442
+ self.foreground,
443
+ self.destructive,
444
+ self.authenticationRequired,
445
+ ),
446
+ )
447
+
448
+
449
+ @dataclass
450
+ class _TextNotificationActionInfo:
451
+ identifier: str
452
+ title: str
453
+ foreground: bool
454
+ destructive: bool
455
+ authenticationRequired: bool
456
+ buttonTitle: str
457
+ textPlaceholder: str
458
+ _extraOptions: UNNotificationCategoryOptions = 0
459
+
460
+ def _makeCallback[T](
461
+ self, appCallback: Callable[[T, str], Awaitable[None]]
462
+ ) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
463
+ async def takesNotification(self: T, response: UNNotificationResponse) -> None:
464
+ await appCallback(self, response.userText())
465
+ return None
466
+
467
+ return takesNotification
468
+
469
+ def _toAction(self) -> UNNotificationAction:
470
+ return UNTextInputNotificationAction.actionWithIdentifier_title_options_textInputButtonTitle_textInputPlaceholder_(
471
+ self.identifier,
472
+ self.title,
473
+ _py2options(
474
+ self.foreground,
475
+ self.destructive,
476
+ self.authenticationRequired,
477
+ ),
478
+ self.buttonTitle,
479
+ self.textPlaceholder,
480
+ )
481
+
482
+
483
+ @dataclass
484
+ class _BuiltinActionInfo:
485
+ identifier: str
486
+ _extraOptions: UNNotificationCategoryOptions
487
+
488
+ def _toAction(self) -> None:
489
+ return None
490
+
491
+ def _makeCallback[T](
492
+ self, appCallback: Callable[[T], Awaitable[None]]
493
+ ) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
494
+ async def takesNotification(self: T, response: UNNotificationResponse) -> None:
495
+ await appCallback(self)
496
+ return None
497
+
498
+ return takesNotification
quickmacapp/_quickapp.py CHANGED
@@ -177,7 +177,7 @@ class QuickApplication(NSApplication):
177
177
  """
178
178
  QuickMacApp's main application class.
179
179
 
180
- @ivar keyEquivalentHandler: Set this attribute to a custom L{NSResponder}
180
+ @ivar keyEquivalentHandler: Set this attribute to a custom C{NSResponder}
181
181
  if you want to handle key equivalents outside the responder chain. (I
182
182
  believe this is necessary in some apps because the responder chain can
183
183
  be more complicated in LSUIElement apps, but there might be a better
@@ -0,0 +1,276 @@
1
+ """
2
+ API for emitting macOS notifications.
3
+
4
+ @see: L{configureNotifications}.
5
+ """
6
+
7
+ from contextlib import AbstractAsyncContextManager as _AbstractAsyncContextManager
8
+ from dataclasses import dataclass as _dataclass
9
+ from typing import Callable, Protocol
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from datetype import DateTime
13
+ from UserNotifications import (
14
+ UNNotificationCategoryOptionCustomDismissAction as _UNNotificationCategoryOptionCustomDismissAction,
15
+ UNNotificationDefaultActionIdentifier as _UNNotificationDefaultActionIdentifier,
16
+ UNNotificationDismissActionIdentifier as _UNNotificationDismissActionIdentifier,
17
+ )
18
+ from UserNotifications import UNUserNotificationCenter as _UNUserNotificationCenter
19
+
20
+ from quickmacapp._notifications import (
21
+ NotificationTranslator,
22
+ _AppNotificationsCtxBuilder,
23
+ _BuiltinActionInfo,
24
+ _PlainNotificationActionInfo,
25
+ _setActionInfo,
26
+ _TextNotificationActionInfo,
27
+ )
28
+
29
+ __all__ = [
30
+ "NotificationTranslator",
31
+ "Action",
32
+ "TextAction",
33
+ "response",
34
+ "Notifier",
35
+ "NotificationConfig",
36
+ ]
37
+
38
+
39
+ class Action[NotificationT](Protocol):
40
+ """
41
+ An action is just an async method that takes its C{self} (an instance of a
42
+ notification class encapsulating the ID & data), and reacts to the
43
+ specified action.
44
+ """
45
+
46
+ async def __call__(__no_self__, /, self: NotificationT) -> None:
47
+ """
48
+ React to the action.
49
+ """
50
+
51
+
52
+ class TextAction[NotificationT](Protocol):
53
+ """
54
+ A L{TextAction} is just like an L{Action}, but it takes some text.
55
+ """
56
+
57
+ async def __call__(__no_self__, /, self: NotificationT, text: str) -> None:
58
+ """
59
+ React to the action with the user's input.
60
+ """
61
+
62
+
63
+ @_dataclass
64
+ class response:
65
+ identifier: str
66
+ title: str
67
+ foreground: bool = False
68
+ destructive: bool = False
69
+ authenticationRequired: bool = False
70
+
71
+ def __call__[NT](self, action: Action[NT], /) -> Action[NT]:
72
+ return _setActionInfo(
73
+ action,
74
+ _PlainNotificationActionInfo(
75
+ identifier=self.identifier,
76
+ title=self.title,
77
+ foreground=self.foreground,
78
+ destructive=self.destructive,
79
+ authenticationRequired=self.authenticationRequired,
80
+ ),
81
+ )
82
+
83
+ def text[NT](
84
+ self, *, title: str | None = None, placeholder: str = ""
85
+ ) -> Callable[[TextAction[NT]], TextAction[NT]]:
86
+ return lambda wrapt: _setActionInfo(
87
+ wrapt,
88
+ _TextNotificationActionInfo(
89
+ identifier=self.identifier,
90
+ title=self.title,
91
+ buttonTitle=title if title is not None else self.title,
92
+ textPlaceholder=placeholder,
93
+ foreground=self.foreground,
94
+ destructive=self.destructive,
95
+ authenticationRequired=self.authenticationRequired,
96
+ ),
97
+ )
98
+
99
+ @staticmethod
100
+ def default[NT]() -> Callable[[Action[NT]], Action[NT]]:
101
+ return lambda wrapt: _setActionInfo(
102
+ wrapt, _BuiltinActionInfo(_UNNotificationDefaultActionIdentifier, 0)
103
+ )
104
+
105
+ @staticmethod
106
+ def dismiss[NT]() -> Callable[[Action[NT]], Action[NT]]:
107
+ return lambda wrapt: _setActionInfo(
108
+ wrapt,
109
+ _BuiltinActionInfo(
110
+ _UNNotificationDismissActionIdentifier,
111
+ _UNNotificationCategoryOptionCustomDismissAction,
112
+ ),
113
+ )
114
+
115
+
116
+ class Notifier[NotifT](Protocol):
117
+ """
118
+ A L{Notifier} can deliver notifications.
119
+ """
120
+
121
+ async def notifyAt(
122
+ self, when: DateTime[ZoneInfo], notification: NotifT, title: str, body: str
123
+ ) -> None:
124
+ """
125
+ Request a future notification at the given time.
126
+ """
127
+
128
+ def undeliver(self, notification: NotifT) -> None:
129
+ """
130
+ Remove the previously-delivered notification object from the
131
+ notification center, if it's still there.
132
+ """
133
+
134
+ def unsend(self, notification: NotifT) -> None:
135
+ """
136
+ Prevent the as-yet undelivered notification object from being
137
+ delivered.
138
+ """
139
+
140
+
141
+ class NotificationConfig(Protocol):
142
+ """
143
+ The application-wide configuration for a notification.
144
+ """
145
+
146
+ def add[NotifT](
147
+ self,
148
+ category: type[NotifT],
149
+ translator: NotificationTranslator[NotifT],
150
+ allowInCarPlay: bool = False,
151
+ hiddenPreviewsShowTitle: bool = False,
152
+ hiddenPreviewsShowSubtitle: bool = False,
153
+ ) -> Notifier[NotifT]:
154
+ """
155
+ Add a new category (represented by a plain Python class, which may have
156
+ some methods that were decorated with L{response}C{(...)},
157
+ L{response.text}C{(...)}).
158
+
159
+ @param category: A Python type that contains the internal state for the
160
+ notifications that will be emitted, that will be relayed back to
161
+ its responses (e.g. the response methods on the category).
162
+
163
+ @param translator: a translator that can serialize and deserialize
164
+ python objects to C{UNUserNotificationCenter} values.
165
+
166
+ @param allowInCarPlay: Should the notification be allowed to show in
167
+ CarPlay?
168
+
169
+ @param hiddenPreviewsShowTitle: Should lock-screen previews for this
170
+ notification show the unredacted title?
171
+
172
+ @param hiddenPreviewsShowSubtitle: Should lock-screen previews for this
173
+ notification show the unredacted subtitle?
174
+
175
+ @return: A L{Notifier} that can deliver notifications to macOS.
176
+
177
+ @note: This method can I{only} be called within the C{with} statement
178
+ for the context manager beneath C{configureNotifications}, and can
179
+ only do this once per process. Otherwise it will raise an
180
+ exception.
181
+ """
182
+
183
+
184
+ def configureNotifications() -> _AbstractAsyncContextManager[NotificationConfig]:
185
+ """
186
+ Configure notifications for the current application.
187
+
188
+ This is an asynchronous (using Twisted's Deferred) context manager, run
189
+ with `with` statement, which works like this::
190
+
191
+ async with configureNotifications() as cfg:
192
+ notifier = cfg.add(MyNotificationData, MyNotificationLoader())
193
+
194
+ Each L{add <NotificationConfig.add>} invocation adds a category of
195
+ notifications you can send, and returns an object (a L{Notifier}) that can
196
+ send that category of notification.
197
+
198
+ At the end of the C{async with} block, the notification configuration is
199
+ finalized, its state is sent to macOS, and the categories of notification
200
+ your application can send is frozen for the rest of the lifetime of your
201
+ process; the L{Notifier} objects returned from L{add
202
+ <NotificationConfig.add>} are now active nad can be used. Note that you
203
+ may only call L{configureNotifications} once in your entire process, so you
204
+ will need to pass those notifiers elsewhere!
205
+
206
+ Each call to add requires 2 arguments: a notification-data class which
207
+ stores the sent notification's ID and any other ancillary data transmitted
208
+ along with it, and an object that can load and store that first class, when
209
+ notification responses from the operating system convey data that was
210
+ previously scheduled as a notification. In our example above, they can be
211
+ as simple as this::
212
+
213
+ class MyNotificationData:
214
+ id: str
215
+
216
+ class MyNotificationLoader:
217
+ def fromNotification(
218
+ self, notificationID: str, userData: dict[str, object]
219
+ ) -> MyNotificationData:
220
+ return MyNotificationData(notificationID)
221
+ def toNotification(
222
+ self,
223
+ notification: MyNotificationData,
224
+ ) -> tuple[str, dict[str, object]]:
225
+ return (notification.id, {})
226
+
227
+ Then, when you want to I{send} a notification, you can do::
228
+
229
+ await notifier.notifyAt(
230
+ aware(datetime.now(TZ) + timedelta(seconds=5), TZ),
231
+ MyNotificationData("my.notification.id.1"),
232
+ "Title Here",
233
+ "Subtitle Here",
234
+ )
235
+
236
+ And that will show the user a notification.
237
+
238
+ The C{MyNotificationData} class might seem simplistic to the point of
239
+ uselessness, and in this oversimplified case, it is! However, if you are
240
+ sending notifications to a user, you really need to be able to I{respond}
241
+ to notifications from a user, and that's where your notification data class
242
+ as well as L{response} comes in. To respond to a notification when the
243
+ user clicks on it, you can add a method like so::
244
+
245
+ class MyNotificationData:
246
+ id: str
247
+
248
+ @response(identifier="response-action-1", title="Action 1")
249
+ async def responseAction1(self) -> None:
250
+ await answer("User pressed 'Action 1' button")
251
+
252
+ @response.default()
253
+ async def userClicked(self) -> None:
254
+ await answer("User clicked the notification.")
255
+
256
+ When sent with L{Notifier.notifyAt}, your C{MyNotificationData} class will
257
+ be serialized and deserialized with C{MyNotificationLoader.toNotification}
258
+ (converting your Python class into a macOS notification, to send along to
259
+ the OS) and C{MyNotificationLoader.fromNotification} (converting the data
260
+ sent along with the user's response back into a C{MyNotificationData}).
261
+
262
+ @note: If your app schedules a notification, then quits, when the user
263
+ responds (clicks on it, uses a button, dismisses it, etc) then the OS
264
+ will re-launch your application and send the notification data back in,
265
+ which is why all the serialization and deserialization is required.
266
+ Your process may have exited and thus the original notification will no
267
+ longer be around. However, if you are just running as a Python script,
268
+ piggybacking on the 'Python Launcher' app bundle, macOS will not be
269
+ able to re-launch your app. Notifications going back to the same
270
+ process seem to work okay, but note that as documented, macOS really
271
+ requires your application to have its own bundle and its own unique
272
+ CFBundleIdentifier in order to avoid any weird behavior.
273
+ """
274
+ return _AppNotificationsCtxBuilder(
275
+ _UNUserNotificationCenter.currentNotificationCenter(), None
276
+ )
@@ -1,13 +1,15 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: quickmacapp
3
- Version: 2025.3.18
3
+ Version: 2025.4.15
4
4
  Summary: Make it easier to write Mac apps in Python
5
5
  Description-Content-Type: text/x-rst
6
6
  License-File: LICENSE
7
7
  Requires-Dist: pyobjc-framework-Cocoa
8
8
  Requires-Dist: pyobjc-framework-ExceptionHandling
9
9
  Requires-Dist: pyobjc-framework-UserNotifications
10
+ Requires-Dist: datetype
10
11
  Requires-Dist: twisted[macos_platform,tls]
12
+ Dynamic: license-file
11
13
 
12
14
  QuickMacApp
13
15
  ==============================
@@ -0,0 +1,12 @@
1
+ quickmacapp/__init__.py,sha256=NsZW_cPvJTunDShN3RjOyK1gTwTKsghMb7wb6vRgTXI,335
2
+ quickmacapp/_background.py,sha256=M_ob3EF8v-Z_gpKuFDl2N9XrQz0eBLFfUp2XErDB3DU,5943
3
+ quickmacapp/_interactions.py,sha256=eLv_mVf5Jr-puNizlR8tDL_DlLy1Rc82XQ80o0GP5R4,3439
4
+ quickmacapp/_notifications.py,sha256=DNm4-vOF0wQNUUTBHqp3-RKXVQK98ef53isxwsWqyj8,16965
5
+ quickmacapp/_quickapp.py,sha256=Ed5n9rAw12nlHxyBMzyPc1Vt4K2OVIqCszWPqQyVElk,7571
6
+ quickmacapp/notifications.py,sha256=UCigVIurf-zWAqVXUYjTtz_xMjcoI23dqyJ4OzZurQM,10275
7
+ quickmacapp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ quickmacapp-2025.4.15.dist-info/licenses/LICENSE,sha256=7RIBNNvrnKHR3lw9z3KXv3Q6RRjhyxtNQoMnoUsf3_M,1091
9
+ quickmacapp-2025.4.15.dist-info/METADATA,sha256=KV6153Iy7rHC5Xrgs8RVtTtBauSRG9GNKzOk3btM-Rg,1344
10
+ quickmacapp-2025.4.15.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
11
+ quickmacapp-2025.4.15.dist-info/top_level.txt,sha256=_iJkekUYnuWhCZbFSQyo2d5_6B7OoPwx7k527bokzeA,12
12
+ quickmacapp-2025.4.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- quickmacapp/__init__.py,sha256=NsZW_cPvJTunDShN3RjOyK1gTwTKsghMb7wb6vRgTXI,335
2
- quickmacapp/_background.py,sha256=M_ob3EF8v-Z_gpKuFDl2N9XrQz0eBLFfUp2XErDB3DU,5943
3
- quickmacapp/_interactions.py,sha256=eLv_mVf5Jr-puNizlR8tDL_DlLy1Rc82XQ80o0GP5R4,3439
4
- quickmacapp/_quickapp.py,sha256=UdZQKIJG40OSzzJ4bdMWix7bpgyZnDAGftF-ksk1VDw,7571
5
- quickmacapp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- quickmacapp-2025.3.18.dist-info/LICENSE,sha256=7RIBNNvrnKHR3lw9z3KXv3Q6RRjhyxtNQoMnoUsf3_M,1091
7
- quickmacapp-2025.3.18.dist-info/METADATA,sha256=7db1LqTDV1pWVXzIEjjubLE7t4_XU4nW4mTxMhMklxo,1298
8
- quickmacapp-2025.3.18.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
9
- quickmacapp-2025.3.18.dist-info/top_level.txt,sha256=_iJkekUYnuWhCZbFSQyo2d5_6B7OoPwx7k527bokzeA,12
10
- quickmacapp-2025.3.18.dist-info/RECORD,,