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.
Files changed (28) hide show
  1. pythonnative/__init__.py +53 -15
  2. pythonnative/cli/pn.py +150 -30
  3. pythonnative/components.py +217 -107
  4. pythonnative/element.py +14 -8
  5. pythonnative/hooks.py +334 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +638 -34
  13. pythonnative/page.py +138 -171
  14. pythonnative/reconciler.py +153 -20
  15. pythonnative/style.py +135 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
  20. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
  21. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
  22. pythonnative/collection_view.py +0 -0
  23. pythonnative/material_bottom_navigation_view.py +0 -0
  24. pythonnative/material_toolbar.py +0 -0
  25. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
  26. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
  27. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
  28. {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