pythonnative 0.37.0__py3-none-any.whl → 0.38.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 +9 -1
- pythonnative/alerts.py +6 -14
- pythonnative/cli/pn.py +96 -10
- pythonnative/components/_base.py +31 -0
- pythonnative/components/controls.py +19 -16
- pythonnative/components/layout.py +5 -8
- pythonnative/components/lists.py +6 -6
- pythonnative/gestures.py +27 -6
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/battery.py +13 -14
- pythonnative/native_modules/biometrics.py +12 -14
- pythonnative/native_modules/camera.py +10 -6
- pythonnative/native_modules/clipboard.py +9 -11
- pythonnative/native_modules/file_system.py +81 -172
- pythonnative/native_modules/haptics.py +14 -10
- pythonnative/native_modules/linking.py +9 -8
- pythonnative/native_modules/location.py +10 -12
- pythonnative/native_modules/net_info.py +1 -4
- pythonnative/native_modules/notifications.py +25 -32
- pythonnative/native_modules/permissions.py +70 -22
- pythonnative/native_modules/secure_store.py +16 -20
- pythonnative/native_modules/share.py +5 -5
- pythonnative/navigation/__init__.py +2 -1
- pythonnative/navigation/hooks.py +46 -7
- pythonnative/navigation/state.py +30 -9
- pythonnative/project/android.py +23 -0
- pythonnative/project/builder.py +64 -35
- pythonnative/project/config.py +84 -24
- pythonnative/project/deps.py +703 -0
- pythonnative/project/doctor.py +53 -4
- pythonnative/project/runtime_assets.py +13 -14
- pythonnative/style.py +94 -44
- pythonnative/templates/android_template/app/build.gradle +5 -2
- pythonnative/templates/android_template/build.gradle +7 -4
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +15 -4
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +25 -10
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +3 -2
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +35 -9
- pythonnative/utils.py +22 -27
- pythonnative/virtual_rows.py +10 -20
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +8 -8
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/RECORD +47 -46
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/top_level.txt +0 -0
|
@@ -38,8 +38,12 @@ class Camera:
|
|
|
38
38
|
``allow_editing``, ...). Unknown keys are ignored.
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
|
-
The saved image path, or ``None`` if the user cancelled or
|
|
42
|
-
no camera
|
|
41
|
+
The saved image path, or ``None`` if the user cancelled (or
|
|
42
|
+
there is no camera to present, as on desktop).
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
NativeModuleError: If the picker can't be presented, for
|
|
46
|
+
example because another picker is already open.
|
|
43
47
|
"""
|
|
44
48
|
return await _launch("take_photo", options)
|
|
45
49
|
|
|
@@ -49,13 +53,13 @@ class Camera:
|
|
|
49
53
|
|
|
50
54
|
Returns:
|
|
51
55
|
The selected image path, or ``None`` if the user cancelled.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
NativeModuleError: If the picker can't be presented.
|
|
52
59
|
"""
|
|
53
60
|
return await _launch("pick_from_gallery", options)
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
async def _launch(method: str, options: Any) -> Optional[str]:
|
|
57
|
-
|
|
58
|
-
result = await native_module("Camera").call_async(method, **options)
|
|
59
|
-
except Exception:
|
|
60
|
-
return None
|
|
64
|
+
result = await native_module("Camera").call_async(method, **options)
|
|
61
65
|
return str(result) if result else None
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[`Clipboard`][pythonnative.Clipboard] reads and writes the system
|
|
4
4
|
pasteboard through the native ``Clipboard`` module (``UIPasteboard``
|
|
5
|
-
on iOS, ``ClipboardManager`` on Android).
|
|
5
|
+
on iOS, ``ClipboardManager`` on Android). The pasteboard lives in
|
|
6
|
+
process memory on both platforms, so every method is synchronous.
|
|
6
7
|
|
|
7
8
|
Off device the module is a process-local string buffer, which keeps it
|
|
8
9
|
usable in ``pn preview`` and unit tests.
|
|
@@ -18,28 +19,25 @@ Example:
|
|
|
18
19
|
|
|
19
20
|
from __future__ import annotations
|
|
20
21
|
|
|
21
|
-
from .. import diagnostics
|
|
22
22
|
from .registry import native_module
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class Clipboard:
|
|
26
|
-
"""System clipboard interface (synchronous).
|
|
26
|
+
"""System clipboard interface (synchronous).
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
NativeModuleError: If the native module reports a failure.
|
|
30
|
+
"""
|
|
27
31
|
|
|
28
32
|
@staticmethod
|
|
29
33
|
def set_string(text: str) -> None:
|
|
30
34
|
"""Copy ``text`` onto the system clipboard."""
|
|
31
|
-
|
|
32
|
-
native_module("Clipboard").call("set_string", text="" if text is None else str(text))
|
|
33
|
-
except Exception:
|
|
34
|
-
diagnostics.swallowed("clipboard.set_string")
|
|
35
|
+
native_module("Clipboard").call("set_string", text="" if text is None else str(text))
|
|
35
36
|
|
|
36
37
|
@staticmethod
|
|
37
38
|
def get_string() -> str:
|
|
38
39
|
"""Return the current clipboard string (``""`` when empty)."""
|
|
39
|
-
|
|
40
|
-
value = native_module("Clipboard").call("get_string")
|
|
41
|
-
except Exception:
|
|
42
|
-
return ""
|
|
40
|
+
value = native_module("Clipboard").call("get_string")
|
|
43
41
|
return "" if value is None else str(value)
|
|
44
42
|
|
|
45
43
|
@staticmethod
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[`
|
|
6
|
-
|
|
1
|
+
"""App-scoped file I/O.
|
|
2
|
+
|
|
3
|
+
[`FileSystem`][pythonnative.FileSystem] answers one question the
|
|
4
|
+
standard library can't, "where may this app write?", and then gets out
|
|
5
|
+
of the way: [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir]
|
|
6
|
+
comes from the native ``Device`` module, and
|
|
7
|
+
[`path`][pythonnative.native_modules.file_system.FileSystem.path] turns
|
|
8
|
+
an app-relative name into a ``pathlib.Path`` you use like any other.
|
|
9
|
+
The read/write helpers are thin conveniences over that path; they
|
|
10
|
+
raise the same ``OSError`` subclasses ``open`` and ``os`` do
|
|
11
|
+
(``FileNotFoundError``, ``PermissionError``, ...) rather than hiding
|
|
12
|
+
them behind ``None`` and ``False``.
|
|
13
|
+
|
|
14
|
+
Relative paths are resolved against ``app_dir``; absolute paths are used
|
|
15
|
+
as-is. Everything here is synchronous: it is local disk I/O on the
|
|
16
|
+
calling thread, exactly like the standard library.
|
|
7
17
|
|
|
8
18
|
Example:
|
|
9
19
|
```python
|
|
@@ -11,36 +21,44 @@ Example:
|
|
|
11
21
|
|
|
12
22
|
FileSystem.write_text("notes/today.txt", "Hello, file system!")
|
|
13
23
|
print(FileSystem.read_text("notes/today.txt"))
|
|
24
|
+
|
|
25
|
+
# Or work with the Path directly:
|
|
26
|
+
notes = FileSystem.path("notes")
|
|
27
|
+
for entry in sorted(notes.iterdir()):
|
|
28
|
+
print(entry.name)
|
|
14
29
|
```
|
|
15
30
|
"""
|
|
16
31
|
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
17
34
|
import os
|
|
18
|
-
from
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, List, Optional, Union
|
|
19
37
|
|
|
20
38
|
from .registry import native_module
|
|
21
39
|
|
|
40
|
+
PathLike = Union[str, "os.PathLike[str]"]
|
|
41
|
+
|
|
22
42
|
_app_dir_cache: Optional[str] = None
|
|
23
43
|
|
|
24
44
|
|
|
25
45
|
class FileSystem:
|
|
26
46
|
"""App-scoped file I/O.
|
|
27
47
|
|
|
28
|
-
Every
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for readers, `False` for writers) so callers can treat the API as
|
|
33
|
-
best-effort.
|
|
48
|
+
Every helper accepts an absolute path or a path relative to
|
|
49
|
+
[`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir],
|
|
50
|
+
and raises ``OSError`` (or a subclass) when the operation fails,
|
|
51
|
+
like the standard library it wraps.
|
|
34
52
|
"""
|
|
35
53
|
|
|
36
54
|
@staticmethod
|
|
37
55
|
def app_dir() -> str:
|
|
38
56
|
"""Return the app's writable data directory.
|
|
39
57
|
|
|
40
|
-
On Android the result is
|
|
41
|
-
the
|
|
42
|
-
either runtime, a
|
|
43
|
-
|
|
58
|
+
On Android the result is ``Context.getFilesDir()``. On iOS it is
|
|
59
|
+
the app's Documents directory. On a desktop machine without
|
|
60
|
+
either runtime, a ``.pythonnative_data`` directory under the
|
|
61
|
+
user's home folder is used. The value comes from the native
|
|
44
62
|
``Device`` module's ``info()`` and is cached after the first
|
|
45
63
|
call.
|
|
46
64
|
|
|
@@ -49,188 +67,79 @@ class FileSystem:
|
|
|
49
67
|
"""
|
|
50
68
|
global _app_dir_cache
|
|
51
69
|
if _app_dir_cache is None:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
info = native_module("Device").call("info")
|
|
55
|
-
if isinstance(info, dict):
|
|
56
|
-
path = info.get("app_dir")
|
|
57
|
-
except Exception:
|
|
58
|
-
path = None
|
|
70
|
+
info = native_module("Device").call("info")
|
|
71
|
+
path = info.get("app_dir") if isinstance(info, dict) else None
|
|
59
72
|
_app_dir_cache = str(path) if path else os.path.join(os.path.expanduser("~"), ".pythonnative_data")
|
|
60
73
|
return _app_dir_cache
|
|
61
74
|
|
|
62
75
|
@staticmethod
|
|
63
|
-
def
|
|
64
|
-
"""
|
|
76
|
+
def path(path: PathLike = "") -> Path:
|
|
77
|
+
"""Resolve ``path`` against ``app_dir`` and return it as a ``pathlib.Path``.
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir].
|
|
69
|
-
encoding: Text encoding (default `"utf-8"`).
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
File contents as a `str`, or `None` if the file cannot be
|
|
73
|
-
read.
|
|
79
|
+
Absolute paths are returned unchanged. With no argument, returns
|
|
80
|
+
``app_dir`` itself.
|
|
74
81
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
with open(full, encoding=encoding) as f:
|
|
78
|
-
return f.read()
|
|
79
|
-
except OSError:
|
|
80
|
-
return None
|
|
82
|
+
candidate = Path(path)
|
|
83
|
+
return candidate if candidate.is_absolute() else Path(FileSystem.app_dir()) / candidate
|
|
81
84
|
|
|
82
85
|
@staticmethod
|
|
83
|
-
def
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
path: Absolute or `app_dir`-relative path.
|
|
88
|
-
content: String to write.
|
|
89
|
-
encoding: Text encoding (default `"utf-8"`).
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
`True` on success, `False` on `OSError`.
|
|
93
|
-
"""
|
|
94
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
95
|
-
try:
|
|
96
|
-
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
97
|
-
with open(full, "w", encoding=encoding) as f:
|
|
98
|
-
f.write(content)
|
|
99
|
-
return True
|
|
100
|
-
except OSError:
|
|
101
|
-
return False
|
|
86
|
+
def read_text(path: PathLike, encoding: str = "utf-8") -> str:
|
|
87
|
+
"""Read a text file; raises ``FileNotFoundError`` and friends like ``open`` does."""
|
|
88
|
+
return FileSystem.path(path).read_text(encoding=encoding)
|
|
102
89
|
|
|
103
90
|
@staticmethod
|
|
104
|
-
def
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
`True` if the path exists.
|
|
112
|
-
"""
|
|
113
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
114
|
-
return os.path.exists(full)
|
|
91
|
+
def write_text(path: PathLike, content: str, encoding: str = "utf-8") -> None:
|
|
92
|
+
"""Write a text file, creating parent directories as needed."""
|
|
93
|
+
target = FileSystem.path(path)
|
|
94
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
target.write_text(content, encoding=encoding)
|
|
115
96
|
|
|
116
97
|
@staticmethod
|
|
117
|
-
def
|
|
118
|
-
"""
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
path: Absolute or `app_dir`-relative path.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
`True` on success, `False` on `OSError`.
|
|
125
|
-
"""
|
|
126
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
127
|
-
try:
|
|
128
|
-
os.remove(full)
|
|
129
|
-
return True
|
|
130
|
-
except OSError:
|
|
131
|
-
return False
|
|
98
|
+
def read_bytes(path: PathLike) -> bytes:
|
|
99
|
+
"""Read a binary file."""
|
|
100
|
+
return FileSystem.path(path).read_bytes()
|
|
132
101
|
|
|
133
102
|
@staticmethod
|
|
134
|
-
def
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
app data directory itself.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
A list of entry names, or an empty list on error.
|
|
143
|
-
"""
|
|
144
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
145
|
-
try:
|
|
146
|
-
return os.listdir(full)
|
|
147
|
-
except OSError:
|
|
148
|
-
return []
|
|
103
|
+
def write_bytes(path: PathLike, data: bytes) -> None:
|
|
104
|
+
"""Write a binary file, creating parent directories as needed."""
|
|
105
|
+
target = FileSystem.path(path)
|
|
106
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
target.write_bytes(data)
|
|
149
108
|
|
|
150
109
|
@staticmethod
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
path: Absolute or `app_dir`-relative path.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
File contents as `bytes`, or `None` if the file cannot be
|
|
159
|
-
read.
|
|
160
|
-
"""
|
|
161
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
162
|
-
try:
|
|
163
|
-
with open(full, "rb") as f:
|
|
164
|
-
return f.read()
|
|
165
|
-
except OSError:
|
|
166
|
-
return None
|
|
110
|
+
def exists(path: PathLike) -> bool:
|
|
111
|
+
"""Return whether a file or directory exists."""
|
|
112
|
+
return FileSystem.path(path).exists()
|
|
167
113
|
|
|
168
114
|
@staticmethod
|
|
169
|
-
def
|
|
170
|
-
"""
|
|
115
|
+
def delete(path: PathLike, *, missing_ok: bool = False) -> None:
|
|
116
|
+
"""Delete a single file.
|
|
171
117
|
|
|
172
118
|
Args:
|
|
173
|
-
path: Absolute or
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
`True` on success, `False` on `OSError`.
|
|
119
|
+
path: Absolute or ``app_dir``-relative path.
|
|
120
|
+
missing_ok: Ignore a missing file instead of raising
|
|
121
|
+
``FileNotFoundError`` (same as ``Path.unlink``).
|
|
178
122
|
"""
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
182
|
-
with open(full, "wb") as f:
|
|
183
|
-
f.write(data)
|
|
184
|
-
return True
|
|
185
|
-
except OSError:
|
|
186
|
-
return False
|
|
123
|
+
FileSystem.path(path).unlink(missing_ok=missing_ok)
|
|
187
124
|
|
|
188
125
|
@staticmethod
|
|
189
|
-
def
|
|
190
|
-
"""Return
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
path: Absolute or `app_dir`-relative path.
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
File size in bytes, or `None` if the file is missing or
|
|
197
|
-
unreadable.
|
|
198
|
-
"""
|
|
199
|
-
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
200
|
-
try:
|
|
201
|
-
return os.path.getsize(full)
|
|
202
|
-
except OSError:
|
|
203
|
-
return None
|
|
126
|
+
def list_dir(path: PathLike = "") -> List[str]:
|
|
127
|
+
"""Return the entry names in a directory (``app_dir`` by default), sorted."""
|
|
128
|
+
return sorted(entry.name for entry in FileSystem.path(path).iterdir())
|
|
204
129
|
|
|
205
130
|
@staticmethod
|
|
206
|
-
def
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
path: Absolute or `app_dir`-relative path.
|
|
131
|
+
def get_size(path: PathLike) -> int:
|
|
132
|
+
"""Return a file's size in bytes."""
|
|
133
|
+
return FileSystem.path(path).stat().st_size
|
|
211
134
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return True
|
|
219
|
-
except OSError:
|
|
220
|
-
return False
|
|
135
|
+
@staticmethod
|
|
136
|
+
def ensure_dir(path: PathLike) -> Path:
|
|
137
|
+
"""Create a directory (and any missing parents) if needed; returns its ``Path``."""
|
|
138
|
+
target = FileSystem.path(path)
|
|
139
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
return target
|
|
221
141
|
|
|
222
142
|
@staticmethod
|
|
223
143
|
def join(*parts: Any) -> str:
|
|
224
|
-
"""Join path components
|
|
225
|
-
|
|
226
|
-
Equivalent to `os.path.join(*map(str, parts))`. Provided as a
|
|
227
|
-
convenience so callers do not need to import `os.path`
|
|
228
|
-
directly.
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
*parts: Path components (each coerced to `str`).
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
The joined path string.
|
|
235
|
-
"""
|
|
144
|
+
"""Join path components with the OS separator (``os.path.join`` over ``str(part)``)."""
|
|
236
145
|
return os.path.join(*[str(p) for p in parts])
|
|
@@ -10,16 +10,15 @@ Two interfaces live here, both backed by the native ``Haptics`` module:
|
|
|
10
10
|
milliseconds" interface for cases where you want an explicit
|
|
11
11
|
duration.
|
|
12
12
|
|
|
13
|
-
Every method is synchronous
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Every method is synchronous (the OS queues the effect and returns).
|
|
14
|
+
Missing hardware is not an error: on a device without a Taptic Engine
|
|
15
|
+
or vibrator, and on desktop, the native module simply does nothing.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
-
from .. import diagnostics
|
|
23
22
|
from .registry import native_module
|
|
24
23
|
|
|
25
24
|
ImpactStyle = str # "light" | "medium" | "heavy" | "soft" | "rigid"
|
|
@@ -27,14 +26,15 @@ NotificationType = str # "success" | "warning" | "error"
|
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
def _call(method: str, **args: Any) -> None:
|
|
30
|
-
|
|
31
|
-
native_module("Haptics").call(method, **args)
|
|
32
|
-
except Exception:
|
|
33
|
-
diagnostics.swallowed(f"haptics.{method}")
|
|
29
|
+
native_module("Haptics").call(method, **args)
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class Haptics:
|
|
37
|
-
"""Semantic haptic feedback (synchronous
|
|
33
|
+
"""Semantic haptic feedback (synchronous).
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
NativeModuleError: If the native module fails.
|
|
37
|
+
"""
|
|
38
38
|
|
|
39
39
|
@staticmethod
|
|
40
40
|
def impact(style: ImpactStyle = "medium") -> None:
|
|
@@ -53,7 +53,11 @@ class Haptics:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
class Vibration:
|
|
56
|
-
"""Raw vibration control (synchronous
|
|
56
|
+
"""Raw vibration control (synchronous).
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
NativeModuleError: If the native module fails.
|
|
60
|
+
"""
|
|
57
61
|
|
|
58
62
|
@staticmethod
|
|
59
63
|
def vibrate(duration_ms: int = 400) -> None:
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
app can hand a URL (``https:``, ``mailto:``, ``tel:``, a custom
|
|
6
6
|
scheme, ...) to the OS.
|
|
7
7
|
|
|
8
|
-
Outbound methods
|
|
9
|
-
whether the platform accepted the
|
|
10
|
-
``False``.
|
|
8
|
+
Outbound methods hand the URL to the OS and return at once, so they
|
|
9
|
+
are synchronous; the ``bool`` says whether the platform accepted the
|
|
10
|
+
request. On desktop they return ``False``.
|
|
11
11
|
|
|
12
12
|
Inbound deep links flow the other way: declare your schemes in
|
|
13
13
|
``pythonnative.toml`` (``app.url_schemes``) and the native module
|
|
@@ -39,14 +39,15 @@ _url_listeners: List[Callable[[str], None]] = []
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def _call_bool(method: str, **args: Any) -> bool:
|
|
42
|
-
|
|
43
|
-
return bool(native_module("Linking").call(method, **args))
|
|
44
|
-
except Exception:
|
|
45
|
-
return False
|
|
42
|
+
return bool(native_module("Linking").call(method, **args))
|
|
46
43
|
|
|
47
44
|
|
|
48
45
|
class Linking:
|
|
49
|
-
"""System URL / deep-link interface (synchronous).
|
|
46
|
+
"""System URL / deep-link interface (synchronous).
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
NativeModuleError: If the native module fails.
|
|
50
|
+
"""
|
|
50
51
|
|
|
51
52
|
@staticmethod
|
|
52
53
|
def open_url(url: str) -> bool:
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[`Location.get_current`][pythonnative.native_modules.location.Location.get_current]
|
|
4
4
|
is a coroutine that resolves to a ``(latitude, longitude)`` tuple, or
|
|
5
|
-
``None`` if no
|
|
5
|
+
``None`` if no fix is available (the user denied permission, location
|
|
6
|
+
services are off, or the request timed out).
|
|
6
7
|
The native ``Location`` module owns the ``CLLocationManager`` /
|
|
7
8
|
``LocationManager`` session and resolves the call with
|
|
8
9
|
``{"latitude", "longitude", "accuracy", "altitude", "timestamp"}``.
|
|
@@ -29,7 +30,7 @@ from __future__ import annotations
|
|
|
29
30
|
|
|
30
31
|
from typing import Any, Dict, Optional, Tuple
|
|
31
32
|
|
|
32
|
-
from .registry import native_module
|
|
33
|
+
from .registry import NativeModuleError, native_module
|
|
33
34
|
|
|
34
35
|
Coords = Tuple[float, float]
|
|
35
36
|
|
|
@@ -48,6 +49,9 @@ class Location:
|
|
|
48
49
|
Returns:
|
|
49
50
|
``(latitude, longitude)`` if a fix was obtained, otherwise
|
|
50
51
|
``None``.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
NativeModuleError: If the native module fails.
|
|
51
55
|
"""
|
|
52
56
|
fix = await Location.get_current_fix(**options)
|
|
53
57
|
if fix is None:
|
|
@@ -62,16 +66,10 @@ class Location:
|
|
|
62
66
|
them ``accuracy`` (meters), ``altitude`` (meters), ``speed``
|
|
63
67
|
(m/s), ``heading`` (degrees), ``timestamp`` (Unix seconds).
|
|
64
68
|
"""
|
|
65
|
-
|
|
66
|
-
result = await native_module("Location").call_async("get_current", **options)
|
|
67
|
-
except Exception:
|
|
68
|
-
return None
|
|
69
|
+
result = await native_module("Location").call_async("get_current", **options)
|
|
69
70
|
if not isinstance(result, dict):
|
|
70
71
|
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fix
|
|
74
|
-
fix["longitude"]
|
|
75
|
-
except (KeyError, TypeError, ValueError):
|
|
76
|
-
return None
|
|
72
|
+
fix: Dict[str, float] = {k: float(v) for k, v in result.items() if v is not None}
|
|
73
|
+
if "latitude" not in fix or "longitude" not in fix:
|
|
74
|
+
raise NativeModuleError("Location", "get_current", f"fix is missing coordinates: {sorted(result)}")
|
|
77
75
|
return fix
|
|
@@ -41,10 +41,7 @@ class NetInfo:
|
|
|
41
41
|
def fetch() -> NetInfoState:
|
|
42
42
|
"""Return a fresh snapshot of connectivity state."""
|
|
43
43
|
global _last_state
|
|
44
|
-
|
|
45
|
-
snapshot = native_module("NetInfo").call("fetch")
|
|
46
|
-
except Exception:
|
|
47
|
-
snapshot = None
|
|
44
|
+
snapshot = native_module("NetInfo").call("fetch")
|
|
48
45
|
if isinstance(snapshot, dict):
|
|
49
46
|
_last_state = _normalize(snapshot)
|
|
50
47
|
return dict(_last_state)
|
|
@@ -39,7 +39,7 @@ from __future__ import annotations
|
|
|
39
39
|
|
|
40
40
|
from typing import Any, Optional
|
|
41
41
|
|
|
42
|
-
from .registry import
|
|
42
|
+
from .registry import native_module
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class Notifications:
|
|
@@ -55,13 +55,13 @@ class Notifications:
|
|
|
55
55
|
is shown if the user hasn't decided yet.
|
|
56
56
|
|
|
57
57
|
Returns:
|
|
58
|
-
``True`` if granted (or no prompt is needed), ``False``
|
|
59
|
-
|
|
58
|
+
``True`` if granted (or no prompt is needed), ``False`` if
|
|
59
|
+
the user declined (always ``False`` on desktop).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
NativeModuleError: If the native module fails.
|
|
60
63
|
"""
|
|
61
|
-
|
|
62
|
-
return bool(await native_module("Notifications").call_async("request_permission"))
|
|
63
|
-
except Exception:
|
|
64
|
-
return False
|
|
64
|
+
return bool(await native_module("Notifications").call_async("request_permission"))
|
|
65
65
|
|
|
66
66
|
@staticmethod
|
|
67
67
|
async def schedule(
|
|
@@ -86,30 +86,26 @@ class Notifications:
|
|
|
86
86
|
``badge``, ...). Unknown keys are ignored.
|
|
87
87
|
|
|
88
88
|
Returns:
|
|
89
|
-
``True``
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
``True`` once scheduled, ``False`` if the user has denied
|
|
90
|
+
notification permission (so nothing was scheduled).
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
NativeModuleError: If the native module fails.
|
|
92
94
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
except Exception:
|
|
103
|
-
return False
|
|
95
|
+
result = await native_module("Notifications").call_async(
|
|
96
|
+
"schedule",
|
|
97
|
+
title=title,
|
|
98
|
+
body=body,
|
|
99
|
+
delay_seconds=float(delay_seconds),
|
|
100
|
+
identifier=identifier,
|
|
101
|
+
**options,
|
|
102
|
+
)
|
|
104
103
|
return bool(result)
|
|
105
104
|
|
|
106
105
|
@staticmethod
|
|
107
106
|
async def cancel(identifier: str = "default") -> None:
|
|
108
|
-
"""Cancel a pending notification by its identifier."""
|
|
109
|
-
|
|
110
|
-
await native_module("Notifications").call_async("cancel", identifier=identifier)
|
|
111
|
-
except Exception:
|
|
112
|
-
pass
|
|
107
|
+
"""Cancel a pending notification by its identifier (a no-op when none is pending)."""
|
|
108
|
+
await native_module("Notifications").call_async("cancel", identifier=identifier)
|
|
113
109
|
|
|
114
110
|
@staticmethod
|
|
115
111
|
async def get_device_token() -> Optional[str]:
|
|
@@ -126,11 +122,8 @@ class Notifications:
|
|
|
126
122
|
remote push support (Android and desktop).
|
|
127
123
|
|
|
128
124
|
Raises:
|
|
129
|
-
|
|
130
|
-
error
|
|
125
|
+
NativeModuleError: If APNs registration fails; ``code`` is
|
|
126
|
+
``"apns"`` and the message carries the system's error.
|
|
131
127
|
"""
|
|
132
|
-
|
|
133
|
-
token = await native_module("Notifications").call_async("get_device_token")
|
|
134
|
-
except NativeModuleError as exc:
|
|
135
|
-
raise RuntimeError(f"APNs registration failed: {exc.message}") from exc
|
|
128
|
+
token = await native_module("Notifications").call_async("get_device_token")
|
|
136
129
|
return str(token) if token else None
|