pythonnative 0.25.0__py3-none-any.whl → 0.27.0__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.
Files changed (50) hide show
  1. pythonnative/__init__.py +1 -1
  2. pythonnative/animated.py +697 -62
  3. pythonnative/cli/pn.py +172 -12
  4. pythonnative/components.py +61 -7
  5. pythonnative/diagnostics.py +27 -0
  6. pythonnative/gestures.py +446 -34
  7. pythonnative/native_modules/app_state.py +2 -1
  8. pythonnative/native_modules/battery.py +3 -2
  9. pythonnative/native_modules/biometrics.py +2 -1
  10. pythonnative/native_modules/camera.py +16 -7
  11. pythonnative/native_modules/clipboard.py +3 -2
  12. pythonnative/native_modules/file_system.py +3 -2
  13. pythonnative/native_modules/haptics.py +7 -6
  14. pythonnative/native_modules/linking.py +62 -6
  15. pythonnative/native_modules/location.py +11 -8
  16. pythonnative/native_modules/net_info.py +75 -5
  17. pythonnative/native_modules/notifications.py +134 -15
  18. pythonnative/native_modules/permissions.py +3 -2
  19. pythonnative/native_modules/share.py +2 -1
  20. pythonnative/native_views/android.py +461 -137
  21. pythonnative/native_views/desktop.py +816 -96
  22. pythonnative/native_views/ios.py +750 -193
  23. pythonnative/project/android.py +15 -3
  24. pythonnative/project/builder.py +172 -70
  25. pythonnative/project/config.py +28 -6
  26. pythonnative/project/devices.py +291 -0
  27. pythonnative/project/doctor.py +5 -4
  28. pythonnative/project/ios.py +51 -77
  29. pythonnative/project/permissions.py +19 -1
  30. pythonnative/project/runtime_assets.py +78 -166
  31. pythonnative/reconciler.py +28 -0
  32. pythonnative/style.py +19 -0
  33. pythonnative/templates/android_template/app/build.gradle +9 -0
  34. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
  35. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
  36. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
  37. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
  38. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  39. pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
  40. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
  41. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
  42. pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
  43. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
  44. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
  45. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
  46. pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
  47. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
  48. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
  49. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
  50. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,22 @@
1
- """Cross-platform local notifications.
1
+ """Cross-platform local notifications and remote push registration.
2
2
 
3
3
  Provides coroutines for requesting permission and scheduling /
4
4
  cancelling local push notifications. Uses Android's
5
5
  ``NotificationManager`` or iOS's ``UNUserNotificationCenter``.
6
6
 
7
7
  On iOS you must ``await Notifications.request_permission()`` before
8
- scheduling. On Android 13+ the runtime permission should be requested
9
- through standard Android APIs (the manifest declaration is otherwise
10
- sufficient).
8
+ scheduling. On Android 13+ the ``POST_NOTIFICATIONS`` runtime
9
+ permission is requested automatically the first time you schedule a
10
+ notification (the template manifest declares it); earlier Android
11
+ versions don't prompt at all.
12
+
13
+ For remote (server-sent) pushes, enable the ``remote_notifications``
14
+ capability in ``pythonnative.toml`` and call
15
+ ``Notifications.get_device_token()`` to register with APNs and receive
16
+ the device token your server needs. Android remote push requires
17
+ Firebase Cloud Messaging, which needs a per-app ``google-services.json``
18
+ and is not wired up by the built-in module; ``get_device_token`` returns
19
+ ``None`` there.
11
20
 
12
21
  Example:
13
22
  ```python
@@ -28,10 +37,11 @@ Example:
28
37
  from __future__ import annotations
29
38
 
30
39
  import asyncio
31
- from typing import Any, Dict
40
+ from typing import Any, Dict, List, Optional
32
41
 
33
- from ..runtime import resolve_future
34
- from ..utils import IS_ANDROID
42
+ from .. import diagnostics
43
+ from ..runtime import reject_future, resolve_future
44
+ from ..utils import IS_ANDROID, IS_IOS
35
45
 
36
46
  # Delayed Android deliveries in flight, keyed by identifier, so
37
47
  # ``cancel`` can abort a notification that hasn't posted yet. (iOS
@@ -46,17 +56,18 @@ class Notifications:
46
56
  async def request_permission() -> bool:
47
57
  """Request notification permission from the user.
48
58
 
49
- On Android the manifest declaration is normally sufficient for
50
- legacy permission grants and this returns ``True`` without
51
- prompting (the runtime POST_NOTIFICATIONS prompt for Android
52
- 13+ should be requested via standard Android APIs).
59
+ On Android 12 and below the manifest declaration is sufficient
60
+ and this returns ``True`` without prompting. On Android 13+
61
+ (API 33) the ``POST_NOTIFICATIONS`` runtime permission is
62
+ requested through [`Permissions`][pythonnative.Permissions],
63
+ which shows the system prompt if the user hasn't decided yet.
53
64
 
54
65
  Returns:
55
66
  ``True`` if granted (or no prompt is needed), ``False``
56
67
  otherwise.
57
68
  """
58
69
  if IS_ANDROID:
59
- return True
70
+ return await _android_request_permission()
60
71
  return await _ios_request_permission()
61
72
 
62
73
  @staticmethod
@@ -83,10 +94,15 @@ class Notifications:
83
94
 
84
95
  Returns:
85
96
  ``True`` on success, ``False`` if the underlying native
86
- call failed.
97
+ call failed or (on Android 13+) the user denied the
98
+ notification permission.
87
99
  """
88
100
  del options
89
101
  if IS_ANDROID:
102
+ # Android 13+ silently drops notifications posted without
103
+ # the POST_NOTIFICATIONS grant, so prompt before posting.
104
+ if not await _android_request_permission():
105
+ return False
90
106
  if delay_seconds > 0:
91
107
  _cancel_android_delayed(identifier)
92
108
  _android_delayed[identifier] = asyncio.ensure_future(
@@ -110,12 +126,55 @@ class Notifications:
110
126
  return
111
127
  await asyncio.to_thread(_ios_cancel, identifier)
112
128
 
129
+ @staticmethod
130
+ async def get_device_token() -> Optional[str]:
131
+ """Register for remote notifications and return the device token.
132
+
133
+ On iOS this calls ``registerForRemoteNotifications`` and waits
134
+ for the APNs callback; the token is a lowercase hex string your
135
+ server passes to APNs. Requires the ``remote_notifications``
136
+ capability (which adds the ``aps-environment`` entitlement) and
137
+ a real device (the simulator has no APNs connection).
138
+
139
+ Returns:
140
+ The APNs token, or ``None`` on platforms without built-in
141
+ remote push support (Android and desktop).
142
+
143
+ Raises:
144
+ RuntimeError: If APNs registration fails, with the native
145
+ error description.
146
+ """
147
+ if not IS_IOS:
148
+ return None
149
+ return await _ios_get_device_token()
150
+
113
151
 
114
152
  # ======================================================================
115
153
  # Android implementation
116
154
  # ======================================================================
117
155
 
118
156
 
157
+ async def _android_request_permission() -> bool:
158
+ """Request ``POST_NOTIFICATIONS`` on Android 13+; older versions skip.
159
+
160
+ Delegates to the shared
161
+ [`Permissions`][pythonnative.Permissions] machinery, which routes
162
+ through the activity's ``requestPermissions`` and resolves once
163
+ ``onRequestPermissionsResult`` arrives. Below API 33 there's no
164
+ runtime permission, so this resolves ``True`` immediately.
165
+ """
166
+ try:
167
+ from java import jclass
168
+
169
+ if int(jclass("android.os.Build$VERSION").SDK_INT) < 33:
170
+ return True
171
+ except Exception:
172
+ return True
173
+ from .permissions import GRANTED, Permissions
174
+
175
+ return await Permissions.request("notifications") == GRANTED
176
+
177
+
119
178
  def _cancel_android_delayed(identifier: str) -> None:
120
179
  pending = _android_delayed.pop(identifier, None)
121
180
  if pending is not None and not pending.done():
@@ -171,7 +230,7 @@ def _android_cancel(identifier: str) -> None:
171
230
  nm = ctx.getSystemService(jclass("android.content.Context").NOTIFICATION_SERVICE)
172
231
  nm.cancel(abs(hash(identifier)) % (2**31))
173
232
  except Exception:
174
- pass
233
+ diagnostics.swallowed("notifications._android_cancel")
175
234
 
176
235
 
177
236
  # ======================================================================
@@ -235,4 +294,64 @@ def _ios_cancel(identifier: str) -> None:
235
294
  arr = NSArray.arrayWithObject_(identifier)
236
295
  center.removePendingNotificationRequestsWithIdentifiers_(arr)
237
296
  except Exception:
238
- pass
297
+ diagnostics.swallowed("notifications._ios_cancel")
298
+
299
+
300
+ # ----------------------------------------------------------------------
301
+ # Remote notifications (APNs)
302
+ # ----------------------------------------------------------------------
303
+ #
304
+ # The registration result arrives through UIApplicationDelegate
305
+ # callbacks in the native template, which forward here via
306
+ # dispatch_device_token / dispatch_device_token_error. The token is
307
+ # cached so later get_device_token() calls resolve immediately.
308
+
309
+ _device_token: Optional[str] = None
310
+ _device_token_error: Optional[str] = None
311
+ _token_waiters: List["asyncio.Future[str]"] = []
312
+
313
+
314
+ async def _ios_get_device_token() -> str:
315
+ if _device_token is not None:
316
+ return _device_token
317
+ if _device_token_error is not None:
318
+ raise RuntimeError(f"APNs registration failed: {_device_token_error}")
319
+
320
+ loop = asyncio.get_running_loop()
321
+ future: asyncio.Future[str] = loop.create_future()
322
+ _token_waiters.append(future)
323
+
324
+ def _register() -> None:
325
+ from rubicon.objc import ObjCClass
326
+
327
+ ObjCClass("UIApplication").sharedApplication.registerForRemoteNotifications()
328
+
329
+ try:
330
+ # registerForRemoteNotifications must run on the main thread.
331
+ from ..runtime import call_on_main_thread
332
+
333
+ call_on_main_thread(_register)
334
+ except Exception as exc:
335
+ _token_waiters.remove(future)
336
+ raise RuntimeError(f"Could not start APNs registration: {exc}") from exc
337
+ return await future
338
+
339
+
340
+ def dispatch_device_token(token: str) -> None:
341
+ """Deliver the APNs device token from the native host."""
342
+ global _device_token, _device_token_error
343
+ _device_token = token
344
+ _device_token_error = None
345
+ waiters, _token_waiters[:] = list(_token_waiters), []
346
+ for future in waiters:
347
+ resolve_future(future, token)
348
+
349
+
350
+ def dispatch_device_token_error(message: str) -> None:
351
+ """Deliver an APNs registration failure from the native host."""
352
+ global _device_token_error
353
+ _device_token_error = message
354
+ waiters, _token_waiters[:] = list(_token_waiters), []
355
+ error = RuntimeError(f"APNs registration failed: {message}")
356
+ for future in waiters:
357
+ reject_future(future, error)
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import asyncio
22
22
  from typing import Any, Callable, Dict
23
23
 
24
+ from .. import diagnostics
24
25
  from ..runtime import resolve_future
25
26
  from ..utils import IS_ANDROID, IS_IOS
26
27
 
@@ -141,7 +142,7 @@ def deliver_android_permission_result(request_code: int, permissions: Any, grant
141
142
  try:
142
143
  cb(status)
143
144
  except Exception:
144
- pass
145
+ diagnostics.swallowed("permissions.deliver_android_permission_result")
145
146
  return True
146
147
 
147
148
 
@@ -215,7 +216,7 @@ def _ios_request(permission: str, on_done: Callable[[PermissionStatus], None]) -
215
216
  center.requestAuthorizationWithOptions_completionHandler_(0x07, Block(_notif_done, None, bool, object))
216
217
  return
217
218
  except Exception:
218
- pass
219
+ diagnostics.swallowed("permissions._ios_request")
219
220
  # Fall back to reporting the current status for permissions whose
220
221
  # request flow isn't wired up natively (location needs a
221
222
  # CLLocationManager delegate; use the Location module's own
@@ -22,6 +22,7 @@ from __future__ import annotations
22
22
  import asyncio
23
23
  from typing import Any, Callable, Dict, Optional
24
24
 
25
+ from .. import diagnostics
25
26
  from ..runtime import resolve_future
26
27
  from ..utils import IS_ANDROID, IS_IOS
27
28
 
@@ -102,7 +103,7 @@ def _ios_share(
102
103
  try:
103
104
  on_done(bool(completed))
104
105
  except Exception:
105
- pass
106
+ diagnostics.swallowed("share._ios_share._completion")
106
107
 
107
108
  controller.setCompletionWithItemsHandler_(
108
109
  Block(_completion, None, ObjCClass("NSString"), Block, ObjCClass("NSArray"), ObjCClass("NSError"))