pythonnative 0.5.0__py3-none-any.whl → 0.7.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 +53 -15
- pythonnative/cli/pn.py +150 -30
- pythonnative/components.py +217 -107
- pythonnative/element.py +14 -8
- pythonnative/hooks.py +334 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +638 -34
- pythonnative/page.py +138 -171
- pythonnative/reconciler.py +153 -20
- pythonnative/style.py +135 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
- pythonnative/collection_view.py +0 -0
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Cross-platform file system access.
|
|
2
|
+
|
|
3
|
+
Provides helpers for reading and writing files within the app's
|
|
4
|
+
sandboxed storage area.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from ..utils import IS_ANDROID
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileSystem:
|
|
14
|
+
"""App-scoped file I/O."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def app_dir() -> str:
|
|
18
|
+
"""Return the app's writable data directory."""
|
|
19
|
+
if IS_ANDROID:
|
|
20
|
+
try:
|
|
21
|
+
from ..utils import get_android_context
|
|
22
|
+
|
|
23
|
+
return str(get_android_context().getFilesDir().getAbsolutePath())
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
else:
|
|
27
|
+
try:
|
|
28
|
+
from rubicon.objc import ObjCClass
|
|
29
|
+
|
|
30
|
+
NSSearchPathForDirectoriesInDomains = ObjCClass(
|
|
31
|
+
"NSFileManager"
|
|
32
|
+
).defaultManager.URLsForDirectory_inDomains_
|
|
33
|
+
docs = NSSearchPathForDirectoriesInDomains(9, 1) # NSDocumentDirectory, NSUserDomainMask
|
|
34
|
+
if docs and docs.count > 0:
|
|
35
|
+
return str(docs.objectAtIndex_(0).path)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return os.path.join(os.path.expanduser("~"), ".pythonnative_data")
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def read_text(path: str, encoding: str = "utf-8") -> Optional[str]:
|
|
42
|
+
"""Read a text file relative to :meth:`app_dir` (or an absolute path)."""
|
|
43
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
44
|
+
try:
|
|
45
|
+
with open(full, encoding=encoding) as f:
|
|
46
|
+
return f.read()
|
|
47
|
+
except OSError:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def write_text(path: str, content: str, encoding: str = "utf-8") -> bool:
|
|
52
|
+
"""Write a text file. Returns ``True`` on success."""
|
|
53
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
54
|
+
try:
|
|
55
|
+
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
56
|
+
with open(full, "w", encoding=encoding) as f:
|
|
57
|
+
f.write(content)
|
|
58
|
+
return True
|
|
59
|
+
except OSError:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def exists(path: str) -> bool:
|
|
64
|
+
"""Check if a file or directory exists."""
|
|
65
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
66
|
+
return os.path.exists(full)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def delete(path: str) -> bool:
|
|
70
|
+
"""Delete a file. Returns ``True`` on success."""
|
|
71
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
72
|
+
try:
|
|
73
|
+
os.remove(full)
|
|
74
|
+
return True
|
|
75
|
+
except OSError:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def list_dir(path: str = "") -> list:
|
|
80
|
+
"""List entries in a directory."""
|
|
81
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
82
|
+
try:
|
|
83
|
+
return os.listdir(full)
|
|
84
|
+
except OSError:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def read_bytes(path: str) -> Optional[bytes]:
|
|
89
|
+
"""Read a binary file."""
|
|
90
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
91
|
+
try:
|
|
92
|
+
with open(full, "rb") as f:
|
|
93
|
+
return f.read()
|
|
94
|
+
except OSError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def write_bytes(path: str, data: bytes) -> bool:
|
|
99
|
+
"""Write a binary file. Returns ``True`` on success."""
|
|
100
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
101
|
+
try:
|
|
102
|
+
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
103
|
+
with open(full, "wb") as f:
|
|
104
|
+
f.write(data)
|
|
105
|
+
return True
|
|
106
|
+
except OSError:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def get_size(path: str) -> Optional[int]:
|
|
111
|
+
"""Return file size in bytes, or ``None`` if not found."""
|
|
112
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
113
|
+
try:
|
|
114
|
+
return os.path.getsize(full)
|
|
115
|
+
except OSError:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def ensure_dir(path: str) -> bool:
|
|
120
|
+
"""Create a directory (and parents) if it doesn't exist."""
|
|
121
|
+
full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
|
|
122
|
+
try:
|
|
123
|
+
os.makedirs(full, exist_ok=True)
|
|
124
|
+
return True
|
|
125
|
+
except OSError:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def join(*parts: Any) -> str:
|
|
130
|
+
"""Join path components."""
|
|
131
|
+
return os.path.join(*[str(p) for p in parts])
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Cross-platform location / GPS access.
|
|
2
|
+
|
|
3
|
+
Provides methods for requesting the current device location.
|
|
4
|
+
Uses Android's ``LocationManager`` or iOS's ``CLLocationManager``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from ..utils import IS_ANDROID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Location:
|
|
13
|
+
"""GPS / Location services interface."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def get_current(
|
|
17
|
+
on_result: Optional[Callable[[Optional[Tuple[float, float]]], None]] = None,
|
|
18
|
+
**options: Any,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Request the current location.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
on_result:
|
|
25
|
+
``((lat, lon) | None) -> None`` called with coordinates or
|
|
26
|
+
``None`` if location is unavailable.
|
|
27
|
+
"""
|
|
28
|
+
if IS_ANDROID:
|
|
29
|
+
Location._android_get(on_result, **options)
|
|
30
|
+
else:
|
|
31
|
+
Location._ios_get(on_result, **options)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _android_get(on_result: Optional[Callable] = None, **options: Any) -> None:
|
|
35
|
+
try:
|
|
36
|
+
from java import jclass
|
|
37
|
+
|
|
38
|
+
from ..utils import get_android_context
|
|
39
|
+
|
|
40
|
+
ctx = get_android_context()
|
|
41
|
+
lm = ctx.getSystemService(jclass("android.content.Context").LOCATION_SERVICE)
|
|
42
|
+
loc = lm.getLastKnownLocation("gps")
|
|
43
|
+
if loc and on_result:
|
|
44
|
+
on_result((loc.getLatitude(), loc.getLongitude()))
|
|
45
|
+
elif on_result:
|
|
46
|
+
on_result(None)
|
|
47
|
+
except Exception:
|
|
48
|
+
if on_result:
|
|
49
|
+
on_result(None)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _ios_get(on_result: Optional[Callable] = None, **options: Any) -> None:
|
|
53
|
+
try:
|
|
54
|
+
from rubicon.objc import ObjCClass
|
|
55
|
+
|
|
56
|
+
lm = ObjCClass("CLLocationManager").alloc().init()
|
|
57
|
+
lm.requestWhenInUseAuthorization()
|
|
58
|
+
lm.startUpdatingLocation()
|
|
59
|
+
except Exception:
|
|
60
|
+
if on_result:
|
|
61
|
+
on_result(None)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Cross-platform local notifications.
|
|
2
|
+
|
|
3
|
+
Provides methods for scheduling and cancelling local push notifications.
|
|
4
|
+
Uses Android's ``NotificationManager`` or iOS's ``UNUserNotificationCenter``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
|
+
|
|
9
|
+
from ..utils import IS_ANDROID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Notifications:
|
|
13
|
+
"""Local notification interface."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def request_permission(on_result: Optional[Callable[[bool], None]] = None) -> None:
|
|
17
|
+
"""Request notification permission from the user.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
on_result:
|
|
22
|
+
``(granted: bool) -> None`` called with the permission result.
|
|
23
|
+
"""
|
|
24
|
+
if IS_ANDROID:
|
|
25
|
+
if on_result:
|
|
26
|
+
on_result(True)
|
|
27
|
+
else:
|
|
28
|
+
Notifications._ios_request_permission(on_result)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def schedule(
|
|
32
|
+
title: str,
|
|
33
|
+
body: str = "",
|
|
34
|
+
delay_seconds: float = 0,
|
|
35
|
+
identifier: str = "default",
|
|
36
|
+
**options: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Schedule a local notification.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
title:
|
|
43
|
+
Notification title.
|
|
44
|
+
body:
|
|
45
|
+
Notification body text.
|
|
46
|
+
delay_seconds:
|
|
47
|
+
Seconds from now until delivery (0 = immediate).
|
|
48
|
+
identifier:
|
|
49
|
+
Unique ID for this notification (for cancellation).
|
|
50
|
+
"""
|
|
51
|
+
if IS_ANDROID:
|
|
52
|
+
Notifications._android_schedule(title, body, delay_seconds, identifier, **options)
|
|
53
|
+
else:
|
|
54
|
+
Notifications._ios_schedule(title, body, delay_seconds, identifier, **options)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def cancel(identifier: str = "default") -> None:
|
|
58
|
+
"""Cancel a pending notification by its identifier."""
|
|
59
|
+
if IS_ANDROID:
|
|
60
|
+
Notifications._android_cancel(identifier)
|
|
61
|
+
else:
|
|
62
|
+
Notifications._ios_cancel(identifier)
|
|
63
|
+
|
|
64
|
+
# -- Android ---------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _android_schedule(title: str, body: str, delay_seconds: float, identifier: str, **options: Any) -> None:
|
|
68
|
+
try:
|
|
69
|
+
from java import jclass
|
|
70
|
+
|
|
71
|
+
from ..utils import get_android_context
|
|
72
|
+
|
|
73
|
+
ctx = get_android_context()
|
|
74
|
+
nm = ctx.getSystemService(jclass("android.content.Context").NOTIFICATION_SERVICE)
|
|
75
|
+
channel_id = "pn_default"
|
|
76
|
+
NotificationChannel = jclass("android.app.NotificationChannel")
|
|
77
|
+
channel = NotificationChannel(channel_id, "PythonNative", 3) # IMPORTANCE_DEFAULT
|
|
78
|
+
nm.createNotificationChannel(channel)
|
|
79
|
+
|
|
80
|
+
Builder = jclass("android.app.Notification$Builder")
|
|
81
|
+
builder = Builder(ctx, channel_id)
|
|
82
|
+
builder.setContentTitle(title)
|
|
83
|
+
builder.setContentText(body)
|
|
84
|
+
builder.setSmallIcon(jclass("android.R$drawable").ic_dialog_info)
|
|
85
|
+
nm.notify(abs(hash(identifier)) % (2**31), builder.build())
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _android_cancel(identifier: str) -> None:
|
|
91
|
+
try:
|
|
92
|
+
from java import jclass
|
|
93
|
+
|
|
94
|
+
from ..utils import get_android_context
|
|
95
|
+
|
|
96
|
+
ctx = get_android_context()
|
|
97
|
+
nm = ctx.getSystemService(jclass("android.content.Context").NOTIFICATION_SERVICE)
|
|
98
|
+
nm.cancel(abs(hash(identifier)) % (2**31))
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# -- iOS -------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _ios_request_permission(on_result: Optional[Callable[[bool], None]] = None) -> None:
|
|
106
|
+
try:
|
|
107
|
+
from rubicon.objc import ObjCClass
|
|
108
|
+
|
|
109
|
+
center = ObjCClass("UNUserNotificationCenter").currentNotificationCenter()
|
|
110
|
+
center.requestAuthorizationWithOptions_completionHandler_(0x07, None)
|
|
111
|
+
if on_result:
|
|
112
|
+
on_result(True)
|
|
113
|
+
except Exception:
|
|
114
|
+
if on_result:
|
|
115
|
+
on_result(False)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _ios_schedule(title: str, body: str, delay_seconds: float, identifier: str, **options: Any) -> None:
|
|
119
|
+
try:
|
|
120
|
+
from rubicon.objc import ObjCClass
|
|
121
|
+
|
|
122
|
+
content = ObjCClass("UNMutableNotificationContent").alloc().init()
|
|
123
|
+
content.setTitle_(title)
|
|
124
|
+
content.setBody_(body)
|
|
125
|
+
|
|
126
|
+
if delay_seconds > 0:
|
|
127
|
+
trigger = ObjCClass("UNTimeIntervalNotificationTrigger").triggerWithTimeInterval_repeats_(
|
|
128
|
+
delay_seconds, False
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
trigger = ObjCClass("UNTimeIntervalNotificationTrigger").triggerWithTimeInterval_repeats_(1, False)
|
|
132
|
+
|
|
133
|
+
request = ObjCClass("UNNotificationRequest").requestWithIdentifier_content_trigger_(
|
|
134
|
+
identifier, content, trigger
|
|
135
|
+
)
|
|
136
|
+
center = ObjCClass("UNUserNotificationCenter").currentNotificationCenter()
|
|
137
|
+
center.addNotificationRequest_withCompletionHandler_(request, None)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _ios_cancel(identifier: str) -> None:
|
|
143
|
+
try:
|
|
144
|
+
from rubicon.objc import ObjCClass
|
|
145
|
+
|
|
146
|
+
center = ObjCClass("UNUserNotificationCenter").currentNotificationCenter()
|
|
147
|
+
NSArray = ObjCClass("NSArray")
|
|
148
|
+
arr = NSArray.arrayWithObject_(identifier)
|
|
149
|
+
center.removePendingNotificationRequestsWithIdentifiers_(arr)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|