pythonnative 0.25.0__py3-none-any.whl → 0.26.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.
- pythonnative/__init__.py +1 -1
- pythonnative/cli/pn.py +172 -12
- pythonnative/diagnostics.py +27 -0
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +2 -1
- pythonnative/native_modules/notifications.py +97 -6
- pythonnative/native_modules/permissions.py +3 -2
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +142 -122
- pythonnative/native_views/desktop.py +50 -49
- pythonnative/native_views/ios.py +228 -160
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/templates/android_template/app/build.gradle +5 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +1 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +88 -12
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +0 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/METADATA +1 -1
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/RECORD +43 -41
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/top_level.txt +0 -0
|
@@ -4,27 +4,38 @@
|
|
|
4
4
|
``Intent(ACTION_VIEW)`` so a Python app can hand a URL (``https:``,
|
|
5
5
|
``mailto:``, ``tel:``, a custom scheme, …) to the OS.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
the platform accepted the request. On desktop they return
|
|
7
|
+
Outbound methods are synchronous and return a ``bool`` describing
|
|
8
|
+
whether the platform accepted the request. On desktop they return
|
|
9
|
+
``False``.
|
|
10
|
+
|
|
11
|
+
Inbound deep links flow the other way: declare your schemes in
|
|
12
|
+
``pythonnative.toml`` (``app.url_schemes``) and the native host calls
|
|
13
|
+
[`dispatch_url`][pythonnative.native_modules.linking.dispatch_url] for
|
|
14
|
+
every URL that opens the app. The URL that cold-started the app is kept
|
|
15
|
+
and returned by ``get_initial_url``; later URLs reach subscribers added
|
|
16
|
+
with ``add_listener``.
|
|
9
17
|
|
|
10
18
|
Example:
|
|
11
19
|
```python
|
|
12
20
|
import pythonnative as pn
|
|
13
21
|
|
|
14
|
-
if pn.Linking.
|
|
15
|
-
|
|
22
|
+
if url := pn.Linking.get_initial_url():
|
|
23
|
+
navigate_to(url)
|
|
24
|
+
|
|
25
|
+
unsubscribe = pn.Linking.add_listener(navigate_to)
|
|
16
26
|
```
|
|
17
27
|
"""
|
|
18
28
|
|
|
19
29
|
from __future__ import annotations
|
|
20
30
|
|
|
21
|
-
from typing import Any, Optional
|
|
31
|
+
from typing import Any, Callable, List, Optional
|
|
22
32
|
|
|
23
33
|
from ..utils import IS_ANDROID, IS_IOS
|
|
24
34
|
|
|
25
35
|
# Populated by the native host when the app is launched from a deep
|
|
26
|
-
# link; ``get_initial_url`` returns it
|
|
36
|
+
# link; ``get_initial_url`` returns it.
|
|
27
37
|
_initial_url: Optional[str] = None
|
|
38
|
+
_url_listeners: List[Callable[[str], None]] = []
|
|
28
39
|
|
|
29
40
|
|
|
30
41
|
class Linking:
|
|
@@ -62,6 +73,28 @@ class Linking:
|
|
|
62
73
|
"""Return the URL that launched the app, if any."""
|
|
63
74
|
return _initial_url
|
|
64
75
|
|
|
76
|
+
@staticmethod
|
|
77
|
+
def add_listener(callback: Callable[[str], None]) -> Callable[[], None]:
|
|
78
|
+
"""Subscribe to deep links that arrive while the app is running.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
callback: Called with the full URL string for every inbound
|
|
82
|
+
deep link (including the initial one, which is
|
|
83
|
+
dispatched right after startup).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A zero-arg function that unsubscribes when called.
|
|
87
|
+
"""
|
|
88
|
+
_url_listeners.append(callback)
|
|
89
|
+
|
|
90
|
+
def _unsubscribe() -> None:
|
|
91
|
+
try:
|
|
92
|
+
_url_listeners.remove(callback)
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return _unsubscribe
|
|
97
|
+
|
|
65
98
|
|
|
66
99
|
def set_initial_url(url: Optional[str]) -> None:
|
|
67
100
|
"""Record the launch URL (called by the native host on cold start)."""
|
|
@@ -69,6 +102,29 @@ def set_initial_url(url: Optional[str]) -> None:
|
|
|
69
102
|
_initial_url = url
|
|
70
103
|
|
|
71
104
|
|
|
105
|
+
def dispatch_url(url: str) -> None:
|
|
106
|
+
"""Deliver an inbound deep link from the native host.
|
|
107
|
+
|
|
108
|
+
The first URL ever dispatched is also recorded as the initial URL
|
|
109
|
+
(a cold start from a deep link reaches Python only after the
|
|
110
|
+
interpreter boots, so the host can't call ``set_initial_url``
|
|
111
|
+
earlier than this).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
url: The full URL string that opened the app.
|
|
115
|
+
"""
|
|
116
|
+
global _initial_url
|
|
117
|
+
if _initial_url is None:
|
|
118
|
+
_initial_url = url
|
|
119
|
+
for listener in list(_url_listeners):
|
|
120
|
+
try:
|
|
121
|
+
listener(url)
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
from .. import diagnostics
|
|
124
|
+
|
|
125
|
+
diagnostics.warn(f"Linking listener raised while handling {url!r}: {exc!r}")
|
|
126
|
+
|
|
127
|
+
|
|
72
128
|
# ======================================================================
|
|
73
129
|
# iOS
|
|
74
130
|
# ======================================================================
|
|
@@ -27,6 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
import asyncio
|
|
28
28
|
from typing import Any, Callable, Dict, Optional, Tuple
|
|
29
29
|
|
|
30
|
+
from .. import diagnostics
|
|
30
31
|
from ..runtime import resolve_future
|
|
31
32
|
from ..utils import IS_ANDROID, IS_IOS
|
|
32
33
|
|
|
@@ -94,7 +95,7 @@ def _ios_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
94
95
|
if self._manager is not None:
|
|
95
96
|
self._manager.stopUpdatingLocation()
|
|
96
97
|
except Exception:
|
|
97
|
-
|
|
98
|
+
diagnostics.swallowed("location._ios_get._PNLocationDelegate._finish")
|
|
98
99
|
cb = self._callback
|
|
99
100
|
self._callback = None
|
|
100
101
|
_pending_delegates.pop(id(self), None)
|
|
@@ -102,7 +103,7 @@ def _ios_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
102
103
|
try:
|
|
103
104
|
cb(coords)
|
|
104
105
|
except Exception:
|
|
105
|
-
|
|
106
|
+
diagnostics.swallowed("location._ios_get._PNLocationDelegate._finish")
|
|
106
107
|
|
|
107
108
|
@objc_method
|
|
108
109
|
def locationManager_didUpdateLocations_(self, manager: Any, locations: Any) -> None:
|
|
@@ -131,7 +132,9 @@ def _ios_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
131
132
|
try:
|
|
132
133
|
manager.startUpdatingLocation()
|
|
133
134
|
except Exception:
|
|
134
|
-
|
|
135
|
+
diagnostics.swallowed(
|
|
136
|
+
"location._ios_get._PNLocationDelegate.locationManagerDidChangeAuthorization_"
|
|
137
|
+
)
|
|
135
138
|
|
|
136
139
|
delegate = _PNLocationDelegate.new()
|
|
137
140
|
delegate._callback = on_result
|
|
@@ -143,11 +146,11 @@ def _ios_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
143
146
|
try:
|
|
144
147
|
manager.requestWhenInUseAuthorization()
|
|
145
148
|
except Exception:
|
|
146
|
-
|
|
149
|
+
diagnostics.swallowed("location._ios_get")
|
|
147
150
|
try:
|
|
148
151
|
manager.startUpdatingLocation()
|
|
149
152
|
except Exception:
|
|
150
|
-
|
|
153
|
+
diagnostics.swallowed("location._ios_get")
|
|
151
154
|
except Exception:
|
|
152
155
|
on_result(None)
|
|
153
156
|
|
|
@@ -176,7 +179,7 @@ def _android_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
176
179
|
on_result((float(loc.getLatitude()), float(loc.getLongitude())))
|
|
177
180
|
return
|
|
178
181
|
except Exception:
|
|
179
|
-
|
|
182
|
+
diagnostics.swallowed("location._android_get")
|
|
180
183
|
|
|
181
184
|
LocationListener = jclass("android.location.LocationListener")
|
|
182
185
|
delivered = [False]
|
|
@@ -188,12 +191,12 @@ def _android_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
188
191
|
try:
|
|
189
192
|
lm.removeUpdates(listener)
|
|
190
193
|
except Exception:
|
|
191
|
-
|
|
194
|
+
diagnostics.swallowed("location._android_get._finish")
|
|
192
195
|
_pending_delegates.pop(id(listener), None)
|
|
193
196
|
try:
|
|
194
197
|
on_result(coords)
|
|
195
198
|
except Exception:
|
|
196
|
-
|
|
199
|
+
diagnostics.swallowed("location._android_get._finish")
|
|
197
200
|
|
|
198
201
|
class _PNLocationListener(dynamic_proxy(LocationListener)): # type: ignore[misc]
|
|
199
202
|
def onLocationChanged(self, loc: Any) -> None:
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
from typing import Callable, Dict, List
|
|
21
21
|
|
|
22
|
+
from .. import diagnostics
|
|
22
23
|
from ..hooks import use_effect, use_state
|
|
23
24
|
from ..utils import IS_ANDROID, IS_IOS
|
|
24
25
|
|
|
@@ -67,7 +68,7 @@ def dispatch_net_info(state: NetInfoState) -> None:
|
|
|
67
68
|
try:
|
|
68
69
|
listener(dict(state))
|
|
69
70
|
except Exception:
|
|
70
|
-
|
|
71
|
+
diagnostics.swallowed("net_info.dispatch_net_info")
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
def use_net_info() -> NetInfoState:
|
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
@@ -9,6 +9,14 @@ scheduling. On Android 13+ the runtime permission should be requested
|
|
|
9
9
|
through standard Android APIs (the manifest declaration is otherwise
|
|
10
10
|
sufficient).
|
|
11
11
|
|
|
12
|
+
For remote (server-sent) pushes, enable the ``remote_notifications``
|
|
13
|
+
capability in ``pythonnative.toml`` and call
|
|
14
|
+
``Notifications.get_device_token()`` to register with APNs and receive
|
|
15
|
+
the device token your server needs. Android remote push requires
|
|
16
|
+
Firebase Cloud Messaging, which needs a per-app ``google-services.json``
|
|
17
|
+
and is not wired up by the built-in module; ``get_device_token`` returns
|
|
18
|
+
``None`` there.
|
|
19
|
+
|
|
12
20
|
Example:
|
|
13
21
|
```python
|
|
14
22
|
import pythonnative as pn
|
|
@@ -28,10 +36,11 @@ Example:
|
|
|
28
36
|
from __future__ import annotations
|
|
29
37
|
|
|
30
38
|
import asyncio
|
|
31
|
-
from typing import Any, Dict
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
32
40
|
|
|
33
|
-
from ..
|
|
34
|
-
from ..
|
|
41
|
+
from .. import diagnostics
|
|
42
|
+
from ..runtime import reject_future, resolve_future
|
|
43
|
+
from ..utils import IS_ANDROID, IS_IOS
|
|
35
44
|
|
|
36
45
|
# Delayed Android deliveries in flight, keyed by identifier, so
|
|
37
46
|
# ``cancel`` can abort a notification that hasn't posted yet. (iOS
|
|
@@ -110,6 +119,28 @@ class Notifications:
|
|
|
110
119
|
return
|
|
111
120
|
await asyncio.to_thread(_ios_cancel, identifier)
|
|
112
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
async def get_device_token() -> Optional[str]:
|
|
124
|
+
"""Register for remote notifications and return the device token.
|
|
125
|
+
|
|
126
|
+
On iOS this calls ``registerForRemoteNotifications`` and waits
|
|
127
|
+
for the APNs callback; the token is a lowercase hex string your
|
|
128
|
+
server passes to APNs. Requires the ``remote_notifications``
|
|
129
|
+
capability (which adds the ``aps-environment`` entitlement) and
|
|
130
|
+
a real device (the simulator has no APNs connection).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The APNs token, or ``None`` on platforms without built-in
|
|
134
|
+
remote push support (Android and desktop).
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
RuntimeError: If APNs registration fails, with the native
|
|
138
|
+
error description.
|
|
139
|
+
"""
|
|
140
|
+
if not IS_IOS:
|
|
141
|
+
return None
|
|
142
|
+
return await _ios_get_device_token()
|
|
143
|
+
|
|
113
144
|
|
|
114
145
|
# ======================================================================
|
|
115
146
|
# Android implementation
|
|
@@ -171,7 +202,7 @@ def _android_cancel(identifier: str) -> None:
|
|
|
171
202
|
nm = ctx.getSystemService(jclass("android.content.Context").NOTIFICATION_SERVICE)
|
|
172
203
|
nm.cancel(abs(hash(identifier)) % (2**31))
|
|
173
204
|
except Exception:
|
|
174
|
-
|
|
205
|
+
diagnostics.swallowed("notifications._android_cancel")
|
|
175
206
|
|
|
176
207
|
|
|
177
208
|
# ======================================================================
|
|
@@ -235,4 +266,64 @@ def _ios_cancel(identifier: str) -> None:
|
|
|
235
266
|
arr = NSArray.arrayWithObject_(identifier)
|
|
236
267
|
center.removePendingNotificationRequestsWithIdentifiers_(arr)
|
|
237
268
|
except Exception:
|
|
238
|
-
|
|
269
|
+
diagnostics.swallowed("notifications._ios_cancel")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ----------------------------------------------------------------------
|
|
273
|
+
# Remote notifications (APNs)
|
|
274
|
+
# ----------------------------------------------------------------------
|
|
275
|
+
#
|
|
276
|
+
# The registration result arrives through UIApplicationDelegate
|
|
277
|
+
# callbacks in the native template, which forward here via
|
|
278
|
+
# dispatch_device_token / dispatch_device_token_error. The token is
|
|
279
|
+
# cached so later get_device_token() calls resolve immediately.
|
|
280
|
+
|
|
281
|
+
_device_token: Optional[str] = None
|
|
282
|
+
_device_token_error: Optional[str] = None
|
|
283
|
+
_token_waiters: List["asyncio.Future[str]"] = []
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def _ios_get_device_token() -> str:
|
|
287
|
+
if _device_token is not None:
|
|
288
|
+
return _device_token
|
|
289
|
+
if _device_token_error is not None:
|
|
290
|
+
raise RuntimeError(f"APNs registration failed: {_device_token_error}")
|
|
291
|
+
|
|
292
|
+
loop = asyncio.get_running_loop()
|
|
293
|
+
future: asyncio.Future[str] = loop.create_future()
|
|
294
|
+
_token_waiters.append(future)
|
|
295
|
+
|
|
296
|
+
def _register() -> None:
|
|
297
|
+
from rubicon.objc import ObjCClass
|
|
298
|
+
|
|
299
|
+
ObjCClass("UIApplication").sharedApplication.registerForRemoteNotifications()
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# registerForRemoteNotifications must run on the main thread.
|
|
303
|
+
from ..runtime import call_on_main_thread
|
|
304
|
+
|
|
305
|
+
call_on_main_thread(_register)
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
_token_waiters.remove(future)
|
|
308
|
+
raise RuntimeError(f"Could not start APNs registration: {exc}") from exc
|
|
309
|
+
return await future
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def dispatch_device_token(token: str) -> None:
|
|
313
|
+
"""Deliver the APNs device token from the native host."""
|
|
314
|
+
global _device_token, _device_token_error
|
|
315
|
+
_device_token = token
|
|
316
|
+
_device_token_error = None
|
|
317
|
+
waiters, _token_waiters[:] = list(_token_waiters), []
|
|
318
|
+
for future in waiters:
|
|
319
|
+
resolve_future(future, token)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def dispatch_device_token_error(message: str) -> None:
|
|
323
|
+
"""Deliver an APNs registration failure from the native host."""
|
|
324
|
+
global _device_token_error
|
|
325
|
+
_device_token_error = message
|
|
326
|
+
waiters, _token_waiters[:] = list(_token_waiters), []
|
|
327
|
+
error = RuntimeError(f"APNs registration failed: {message}")
|
|
328
|
+
for future in waiters:
|
|
329
|
+
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"))
|