@pyscript/core 0.7.11 → 0.7.13
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.
- package/dist/{codemirror-BYspKCDy.js → codemirror-lkW_eC9r.js} +2 -2
- package/dist/codemirror-lkW_eC9r.js.map +1 -0
- package/dist/{codemirror_commands-BLDaEdQ6.js → codemirror_commands-DL2aL4qa.js} +2 -2
- package/dist/codemirror_commands-DL2aL4qa.js.map +1 -0
- package/dist/codemirror_lang-python-DD5EtV36.js +2 -0
- package/dist/codemirror_lang-python-DD5EtV36.js.map +1 -0
- package/dist/codemirror_language-DRHeqAwG.js +2 -0
- package/dist/codemirror_language-DRHeqAwG.js.map +1 -0
- package/dist/codemirror_state-BNbbkoNK.js +2 -0
- package/dist/codemirror_state-BNbbkoNK.js.map +1 -0
- package/dist/codemirror_view-FN7LalDk.js +2 -0
- package/dist/codemirror_view-FN7LalDk.js.map +1 -0
- package/dist/core-B4BRXuDy.js +4 -0
- package/dist/core-B4BRXuDy.js.map +1 -0
- package/dist/core.js +1 -1
- package/dist/{deprecations-manager-DIDxhyRq.js → deprecations-manager-BRHTwqUZ.js} +2 -2
- package/dist/{deprecations-manager-DIDxhyRq.js.map → deprecations-manager-BRHTwqUZ.js.map} +1 -1
- package/dist/{donkey-CLhmQOjG.js → donkey-CBEqGHeD.js} +2 -2
- package/dist/{donkey-CLhmQOjG.js.map → donkey-CBEqGHeD.js.map} +1 -1
- package/dist/{error-uzvvriog.js → error-DRVc1NKK.js} +2 -2
- package/dist/{error-uzvvriog.js.map → error-DRVc1NKK.js.map} +1 -1
- package/dist/index-C-U2wRvV.js +2 -0
- package/dist/index-C-U2wRvV.js.map +1 -0
- package/dist/{mpy-CnF17tqI.js → mpy-B-jI5Qug.js} +2 -2
- package/dist/{mpy-CnF17tqI.js.map → mpy-B-jI5Qug.js.map} +1 -1
- package/dist/{py-BZSSqcx3.js → py-DNLpCVR2.js} +2 -2
- package/dist/{py-BZSSqcx3.js.map → py-DNLpCVR2.js.map} +1 -1
- package/dist/{py-editor-DZ0Dxzzk.js → py-editor-DCtATRBs.js} +2 -2
- package/dist/{py-editor-DZ0Dxzzk.js.map → py-editor-DCtATRBs.js.map} +1 -1
- package/dist/{py-game-bqieV522.js → py-game-BGWt8dH1.js} +2 -2
- package/dist/{py-game-bqieV522.js.map → py-game-BGWt8dH1.js.map} +1 -1
- package/dist/{py-terminal-DYY4WN57.js → py-terminal-BKvzGq7q.js} +2 -2
- package/dist/{py-terminal-DYY4WN57.js.map → py-terminal-BKvzGq7q.js.map} +1 -1
- package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
- package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
- package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
- package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
- package/dist/zip-CgZGjqjF.js +2 -0
- package/dist/zip-CgZGjqjF.js.map +1 -0
- package/package.json +15 -14
- package/src/3rd-party/xterm_addon-fit.js +14 -2
- package/src/3rd-party/xterm_addon-web-links.js +14 -2
- package/src/core.js +13 -2
- package/src/stdlib/pyscript/__init__.py +100 -31
- package/src/stdlib/pyscript/context.py +198 -0
- package/src/stdlib/pyscript/display.py +211 -127
- package/src/stdlib/pyscript/events.py +191 -88
- package/src/stdlib/pyscript/fetch.py +156 -25
- package/src/stdlib/pyscript/ffi.py +132 -16
- package/src/stdlib/pyscript/flatted.py +78 -1
- package/src/stdlib/pyscript/fs.py +205 -49
- package/src/stdlib/pyscript/media.py +210 -50
- package/src/stdlib/pyscript/storage.py +214 -27
- package/src/stdlib/pyscript/util.py +28 -7
- package/src/stdlib/pyscript/web.py +1079 -881
- package/src/stdlib/pyscript/websocket.py +252 -45
- package/src/stdlib/pyscript/workers.py +176 -27
- package/src/stdlib/pyscript.js +13 -13
- package/types/stdlib/pyscript.d.ts +1 -1
- package/dist/codemirror-BYspKCDy.js.map +0 -1
- package/dist/codemirror_commands-BLDaEdQ6.js.map +0 -1
- package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
- package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
- package/dist/codemirror_language-DOkvasqm.js +0 -2
- package/dist/codemirror_language-DOkvasqm.js.map +0 -1
- package/dist/codemirror_state-BIAL8JKm.js +0 -2
- package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
- package/dist/codemirror_view-Bt4sLgyA.js +0 -2
- package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
- package/dist/core-PTfg6inS.js +0 -4
- package/dist/core-PTfg6inS.js.map +0 -1
- package/dist/index-jZ1aOVVJ.js +0 -2
- package/dist/index-jZ1aOVVJ.js.map +0 -1
- package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
- package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
- package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
- package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
- package/dist/zip-pccs084i.js +0 -2
- package/dist/zip-pccs084i.js.map +0 -1
- package/src/stdlib/pyscript/magic_js.py +0 -84
|
@@ -1,87 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides classes and functions for interacting with
|
|
3
|
+
[media devices and streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API)
|
|
4
|
+
in the browser, enabling you to work with cameras, microphones,
|
|
5
|
+
and other media input/output devices directly from Python.
|
|
6
|
+
|
|
7
|
+
Use this module for:
|
|
8
|
+
|
|
9
|
+
- Accessing webcams for video capture.
|
|
10
|
+
- Recording audio from microphones.
|
|
11
|
+
- Enumerating available media devices.
|
|
12
|
+
- Applying constraints to media streams (resolution, frame rate, etc.).
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from pyscript import document
|
|
16
|
+
from pyscript.media import Device, list_devices
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Get a video stream from the default camera.
|
|
20
|
+
stream = await Device.request_stream(video=True)
|
|
21
|
+
|
|
22
|
+
# Display in a video element.
|
|
23
|
+
video = document.getElementById("my-video")
|
|
24
|
+
video.srcObject = stream
|
|
25
|
+
|
|
26
|
+
# Or list all available devices.
|
|
27
|
+
devices = await list_devices()
|
|
28
|
+
for device in devices:
|
|
29
|
+
print(f"{device.kind}: {device.label}")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Using media devices requires user permission. Browsers will show a
|
|
33
|
+
permission dialog when accessing devices for the first time.
|
|
34
|
+
"""
|
|
35
|
+
|
|
1
36
|
from pyscript import window
|
|
2
37
|
from pyscript.ffi import to_js
|
|
3
38
|
|
|
4
39
|
|
|
5
40
|
class Device:
|
|
6
|
-
"""
|
|
7
|
-
|
|
41
|
+
"""
|
|
42
|
+
Represents a media input or output device.
|
|
43
|
+
|
|
44
|
+
This class wraps a browser
|
|
45
|
+
[MediaDeviceInfo object](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo),
|
|
46
|
+
providing Pythonic access to device properties like `ID`, `label`, and
|
|
47
|
+
`kind` (audio/video, input/output).
|
|
48
|
+
|
|
49
|
+
Devices are typically obtained via the `list_devices()` function in this
|
|
50
|
+
module, rather than constructed directly.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from pyscript.media import list_devices
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Get all available devices.
|
|
57
|
+
devices = await list_devices()
|
|
58
|
+
|
|
59
|
+
# Find video input devices (cameras).
|
|
60
|
+
cameras = [d for d in devices if d.kind == "videoinput"]
|
|
61
|
+
|
|
62
|
+
# Get a stream from a specific camera.
|
|
63
|
+
if cameras:
|
|
64
|
+
stream = await cameras[0].get_stream()
|
|
65
|
+
```
|
|
8
66
|
"""
|
|
9
67
|
|
|
10
68
|
def __init__(self, device):
|
|
11
|
-
|
|
69
|
+
"""
|
|
70
|
+
Create a Device wrapper around a MediaDeviceInfo `device`.
|
|
71
|
+
"""
|
|
72
|
+
self._device_info = device
|
|
12
73
|
|
|
13
74
|
@property
|
|
14
75
|
def id(self):
|
|
15
|
-
|
|
76
|
+
"""
|
|
77
|
+
Unique identifier for this device.
|
|
78
|
+
|
|
79
|
+
This `ID` persists across sessions but is reset when the user clears
|
|
80
|
+
cookies. It's unique to the origin of the calling application.
|
|
81
|
+
"""
|
|
82
|
+
return self._device_info.deviceId
|
|
16
83
|
|
|
17
84
|
@property
|
|
18
85
|
def group(self):
|
|
19
|
-
|
|
86
|
+
"""
|
|
87
|
+
Group identifier for related devices.
|
|
88
|
+
|
|
89
|
+
Devices belonging to the same physical device (e.g., a monitor with
|
|
90
|
+
both a camera and microphone) share the same `group ID`.
|
|
91
|
+
"""
|
|
92
|
+
return self._device_info.groupId
|
|
20
93
|
|
|
21
94
|
@property
|
|
22
95
|
def kind(self):
|
|
23
|
-
|
|
96
|
+
"""
|
|
97
|
+
Device type: `"videoinput"`, `"audioinput"`, or `"audiooutput"`.
|
|
98
|
+
"""
|
|
99
|
+
return self._device_info.kind
|
|
24
100
|
|
|
25
101
|
@property
|
|
26
102
|
def label(self):
|
|
27
|
-
|
|
103
|
+
"""
|
|
104
|
+
Human-readable description of the device.
|
|
105
|
+
|
|
106
|
+
Example: `"External USB Webcam"` or `"Built-in Microphone"`.
|
|
107
|
+
"""
|
|
108
|
+
return self._device_info.label
|
|
28
109
|
|
|
29
110
|
def __getitem__(self, key):
|
|
111
|
+
"""
|
|
112
|
+
Support bracket notation for JavaScript interop.
|
|
113
|
+
|
|
114
|
+
Allows accessing properties via `device["id"]` syntax. Necessary
|
|
115
|
+
when Device instances are proxied to JavaScript.
|
|
116
|
+
"""
|
|
30
117
|
return getattr(self, key)
|
|
31
118
|
|
|
32
119
|
@classmethod
|
|
33
|
-
async def
|
|
120
|
+
async def request_stream(cls, audio=False, video=True):
|
|
34
121
|
"""
|
|
35
|
-
|
|
122
|
+
Request a media stream with the specified constraints.
|
|
123
|
+
|
|
124
|
+
This is a class method that requests access to media devices matching
|
|
125
|
+
the given `audio` and `video` constraints. The browser will prompt the
|
|
126
|
+
user for permission if needed and return a `MediaStream` object that
|
|
127
|
+
can be assigned to video/audio elements.
|
|
128
|
+
|
|
129
|
+
Simple boolean constraints for `audio` and `video` can be used to
|
|
130
|
+
request default devices. More complex constraints can be specified as
|
|
131
|
+
dictionaries conforming to
|
|
132
|
+
[the MediaTrackConstraints interface](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pyscript import document
|
|
136
|
+
from pyscript.media import Device
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Get default video stream.
|
|
140
|
+
stream = await Device.request_stream()
|
|
141
|
+
|
|
142
|
+
# Get stream with specific constraints.
|
|
143
|
+
stream = await Device.request_stream(
|
|
144
|
+
video={"width": 1920, "height": 1080}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Get audio and video.
|
|
148
|
+
stream = await Device.request_stream(audio=True, video=True)
|
|
149
|
+
|
|
150
|
+
# Use the stream.
|
|
151
|
+
video_el = document.getElementById("camera")
|
|
152
|
+
video_el.srcObject = stream
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This method will trigger a browser permission dialog on first use.
|
|
36
156
|
"""
|
|
37
157
|
options = {}
|
|
38
|
-
|
|
158
|
+
if isinstance(audio, bool):
|
|
159
|
+
options["audio"] = audio
|
|
160
|
+
elif isinstance(audio, dict):
|
|
161
|
+
# audio is a dict of constraints (sampleRate, echoCancellation etc...).
|
|
162
|
+
options["audio"] = audio
|
|
39
163
|
if isinstance(video, bool):
|
|
40
164
|
options["video"] = video
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
options["video"][k] = video[k]
|
|
165
|
+
elif isinstance(video, dict):
|
|
166
|
+
# video is a dict of constraints (width, height etc...).
|
|
167
|
+
options["video"] = video
|
|
45
168
|
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
|
46
169
|
|
|
170
|
+
@classmethod
|
|
171
|
+
async def load(cls, audio=False, video=True):
|
|
172
|
+
"""
|
|
173
|
+
!!! warning
|
|
174
|
+
**Deprecated: Use `request_stream()` instead.**
|
|
175
|
+
|
|
176
|
+
This method is retained for backwards compatibility but will be
|
|
177
|
+
removed in a future release. Please use `request_stream()` instead.
|
|
178
|
+
"""
|
|
179
|
+
return await cls.request_stream(audio=audio, video=video)
|
|
180
|
+
|
|
47
181
|
async def get_stream(self):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
182
|
+
"""
|
|
183
|
+
Get a media stream from this specific device.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pyscript.media import list_devices
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# List all devices.
|
|
190
|
+
devices = await list_devices()
|
|
191
|
+
|
|
192
|
+
# Find a specific camera.
|
|
193
|
+
my_camera = None
|
|
194
|
+
for device in devices:
|
|
195
|
+
if device.kind == "videoinput" and "USB" in device.label:
|
|
196
|
+
my_camera = device
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Get a stream from that specific camera.
|
|
200
|
+
if my_camera:
|
|
201
|
+
stream = await my_camera.get_stream()
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This will trigger a permission dialog if the user hasn't already
|
|
205
|
+
granted permission for this device type.
|
|
206
|
+
"""
|
|
207
|
+
# Extract media type from device kind (e.g., "videoinput" -> "video").
|
|
208
|
+
media_type = self.kind.replace("input", "").replace("output", "")
|
|
209
|
+
# Request stream with exact device ID constraint.
|
|
210
|
+
options = {media_type: {"deviceId": {"exact": self.id}}}
|
|
211
|
+
return await self.request_stream(**options)
|
|
51
212
|
|
|
52
213
|
|
|
53
|
-
async def list_devices()
|
|
214
|
+
async def list_devices():
|
|
54
215
|
"""
|
|
55
|
-
|
|
56
|
-
such as microphones, cameras,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
216
|
+
Returns a list of all media devices currently available to the browser,
|
|
217
|
+
such as microphones, cameras, and speakers.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from pyscript.media import list_devices
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# Get all devices.
|
|
224
|
+
devices = await list_devices()
|
|
225
|
+
|
|
226
|
+
# Print device information.
|
|
227
|
+
for device in devices:
|
|
228
|
+
print(f"{device.kind}: {device.label} (ID: {device.id})")
|
|
229
|
+
|
|
230
|
+
# Filter for specific device types.
|
|
231
|
+
cameras = [d for d in devices if d.kind == "videoinput"]
|
|
232
|
+
microphones = [d for d in devices if d.kind == "audioinput"]
|
|
233
|
+
speakers = [d for d in devices if d.kind == "audiooutput"]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The returned list will omit devices that are blocked by the document
|
|
237
|
+
[Permission Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy)
|
|
238
|
+
(microphone, camera, speaker-selection) or for
|
|
239
|
+
which the user has not granted explicit permission.
|
|
240
|
+
|
|
241
|
+
For security and privacy, device labels may be empty strings until
|
|
242
|
+
permission is granted. See
|
|
243
|
+
[this document](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices)
|
|
244
|
+
for more information about this web standard.
|
|
83
245
|
"""
|
|
84
|
-
|
|
85
|
-
return [
|
|
86
|
-
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
|
87
|
-
]
|
|
246
|
+
device_infos = await window.navigator.mediaDevices.enumerateDevices()
|
|
247
|
+
return [Device(device_info) for device_info in device_infos]
|
|
@@ -1,11 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module wraps the browser's
|
|
3
|
+
[IndexedDB persistent storage](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
|
4
|
+
to provide a familiar Python dictionary API. Data is automatically
|
|
5
|
+
serialized and persisted, surviving page reloads and browser restarts.
|
|
6
|
+
|
|
7
|
+
Storage is persistent per origin (domain), isolated between different sites
|
|
8
|
+
for security. Browsers typically allow each origin to store up to 10-60% of
|
|
9
|
+
total disk space, depending on browser and configuration.
|
|
10
|
+
|
|
11
|
+
What this module provides:
|
|
12
|
+
|
|
13
|
+
- A `dict`-like API (get, set, delete, iterate).
|
|
14
|
+
- Automatic serialization of common Python types.
|
|
15
|
+
- Background persistence with optional explicit `sync()`.
|
|
16
|
+
- Support for custom `Storage` subclasses.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from pyscript import storage
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Create or open a named storage.
|
|
23
|
+
my_data = await storage("user-preferences")
|
|
24
|
+
|
|
25
|
+
# Use like a regular dictionary.
|
|
26
|
+
my_data["theme"] = "dark"
|
|
27
|
+
my_data["font_size"] = 14
|
|
28
|
+
my_data["settings"] = {"notifications": True, "sound": False}
|
|
29
|
+
|
|
30
|
+
# Changes are queued automatically.
|
|
31
|
+
# To ensure immediate write, sync explicitly.
|
|
32
|
+
await my_data.sync()
|
|
33
|
+
|
|
34
|
+
# Read values (survives page reload).
|
|
35
|
+
theme = my_data.get("theme", "light")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Common types are automatically serialized: `bool`, `int`, `float`, `str`, `None`,
|
|
39
|
+
`list`, `dict`, `tuple`. Binary data (`bytearray`, `memoryview`) can be stored as
|
|
40
|
+
single values but not nested in structures.
|
|
41
|
+
|
|
42
|
+
Tuples are deserialized as lists due to IndexedDB limitations.
|
|
43
|
+
|
|
44
|
+
!!! info
|
|
45
|
+
Browsers typically allow 10-60% of total disk space per origin. Chrome
|
|
46
|
+
and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
|
|
47
|
+
is smaller). Safari varies by app type. These limits are unlikely to be
|
|
48
|
+
reached in typical usage.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from polyscript import storage as _polyscript_storage
|
|
2
52
|
from pyscript.flatted import parse as _parse
|
|
3
53
|
from pyscript.flatted import stringify as _stringify
|
|
4
54
|
from pyscript.ffi import is_none
|
|
5
55
|
|
|
6
56
|
|
|
7
|
-
|
|
8
|
-
|
|
57
|
+
def _convert_to_idb(value):
|
|
58
|
+
"""
|
|
59
|
+
Convert a Python `value` to an IndexedDB-compatible format.
|
|
60
|
+
|
|
61
|
+
Values are serialized using Flatted (for circular reference support)
|
|
62
|
+
with type information to enable proper deserialization. It returns a
|
|
63
|
+
JSON string representing the serialized value.
|
|
64
|
+
|
|
65
|
+
Will raise a TypeError if the value type is not supported.
|
|
66
|
+
"""
|
|
9
67
|
if is_none(value):
|
|
10
68
|
return _stringify(["null", 0])
|
|
11
69
|
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
|
@@ -14,50 +72,179 @@ def _to_idb(value):
|
|
|
14
72
|
return _stringify(["bytearray", list(value)])
|
|
15
73
|
if isinstance(value, memoryview):
|
|
16
74
|
return _stringify(["memoryview", list(value)])
|
|
17
|
-
|
|
18
|
-
raise TypeError(msg)
|
|
75
|
+
raise TypeError(f"Cannot serialize type {type(value).__name__} for storage.")
|
|
19
76
|
|
|
20
77
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
78
|
+
def _convert_from_idb(value):
|
|
79
|
+
"""
|
|
80
|
+
Convert an IndexedDB `value` back to its Python representation.
|
|
81
|
+
|
|
82
|
+
Uses type information stored during serialization to reconstruct the
|
|
83
|
+
original Python type.
|
|
84
|
+
"""
|
|
85
|
+
kind, data = _parse(value)
|
|
86
|
+
|
|
27
87
|
if kind == "null":
|
|
28
88
|
return None
|
|
29
89
|
if kind == "generic":
|
|
30
|
-
return
|
|
90
|
+
return data
|
|
31
91
|
if kind == "bytearray":
|
|
32
|
-
return bytearray(
|
|
92
|
+
return bytearray(data)
|
|
33
93
|
if kind == "memoryview":
|
|
34
|
-
return memoryview(bytearray(
|
|
94
|
+
return memoryview(bytearray(data))
|
|
95
|
+
# Fallback for all other types.
|
|
35
96
|
return value
|
|
36
97
|
|
|
37
98
|
|
|
38
99
|
class Storage(dict):
|
|
100
|
+
"""
|
|
101
|
+
A persistent dictionary backed by the browser's IndexedDB.
|
|
102
|
+
|
|
103
|
+
This class provides a dict-like interface with automatic persistence.
|
|
104
|
+
Changes are queued for background writing, with optional explicit
|
|
105
|
+
synchronization via `sync()`.
|
|
106
|
+
|
|
107
|
+
Inherits from `dict`, so all standard dictionary methods work as expected.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from pyscript import storage
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Open a storage.
|
|
114
|
+
prefs = await storage("preferences")
|
|
115
|
+
|
|
116
|
+
# Use as a dictionary.
|
|
117
|
+
prefs["color"] = "blue"
|
|
118
|
+
prefs["count"] = 42
|
|
119
|
+
|
|
120
|
+
# Iterate like a dict.
|
|
121
|
+
for key, value in prefs.items():
|
|
122
|
+
print(f"{key}: {value}")
|
|
123
|
+
|
|
124
|
+
# Ensure writes complete immediately.
|
|
125
|
+
await prefs.sync()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Sometimes you may need to subclass `Storage` to add custom behavior:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from pyscript import storage, Storage, window
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LoggingStorage(Storage):
|
|
135
|
+
def __setitem__(self, key, value):
|
|
136
|
+
window.console.log(f"Setting {key} = {value}")
|
|
137
|
+
super().__setitem__(key, value)
|
|
138
|
+
|
|
139
|
+
my_store = await storage("app-data", storage_class=LoggingStorage)
|
|
140
|
+
my_store["test"] = 123 # Logs to console.
|
|
141
|
+
```
|
|
142
|
+
"""
|
|
143
|
+
|
|
39
144
|
def __init__(self, store):
|
|
40
|
-
|
|
41
|
-
|
|
145
|
+
"""
|
|
146
|
+
Create a Storage instance wrapping an IndexedDB `store` (a JS
|
|
147
|
+
proxy).
|
|
148
|
+
"""
|
|
149
|
+
super().__init__(
|
|
150
|
+
{key: _convert_from_idb(value) for key, value in store.entries()}
|
|
151
|
+
)
|
|
152
|
+
self._store = store
|
|
153
|
+
|
|
154
|
+
def __delitem__(self, key):
|
|
155
|
+
"""
|
|
156
|
+
Delete an item from storage via its `key`.
|
|
157
|
+
|
|
158
|
+
The deletion is queued for persistence. Use `sync()` to ensure
|
|
159
|
+
immediate completion.
|
|
160
|
+
"""
|
|
161
|
+
self._store.delete(key)
|
|
162
|
+
super().__delitem__(key)
|
|
42
163
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
164
|
+
def __setitem__(self, key, value):
|
|
165
|
+
"""
|
|
166
|
+
Set a `key` to a `value` in storage.
|
|
46
167
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
168
|
+
The change is queued for persistence. Use `sync()` to ensure
|
|
169
|
+
immediate completion. The `value` must be a supported type for
|
|
170
|
+
serialization.
|
|
171
|
+
"""
|
|
172
|
+
self._store.set(key, _convert_to_idb(value))
|
|
173
|
+
super().__setitem__(key, value)
|
|
50
174
|
|
|
51
175
|
def clear(self):
|
|
52
|
-
|
|
176
|
+
"""
|
|
177
|
+
Remove all items from storage.
|
|
178
|
+
|
|
179
|
+
The `clear()` operation is queued for persistence. Use `sync()` to ensure
|
|
180
|
+
immediate completion.
|
|
181
|
+
"""
|
|
182
|
+
self._store.clear()
|
|
53
183
|
super().clear()
|
|
54
184
|
|
|
55
185
|
async def sync(self):
|
|
56
|
-
|
|
186
|
+
"""
|
|
187
|
+
Force immediate synchronization to IndexedDB.
|
|
188
|
+
|
|
189
|
+
By default, storage operations are queued and written asynchronously.
|
|
190
|
+
Call `sync()` when you need to guarantee changes are persisted immediately,
|
|
191
|
+
such as before critical operations or page unload.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
store = await storage("important-data")
|
|
195
|
+
store["critical_value"] = data
|
|
196
|
+
|
|
197
|
+
# Ensure it's written before proceeding.
|
|
198
|
+
await store.sync()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This is a blocking operation that waits for IndexedDB to complete
|
|
202
|
+
the write.
|
|
203
|
+
"""
|
|
204
|
+
await self._store.sync()
|
|
57
205
|
|
|
58
206
|
|
|
59
207
|
async def storage(name="", storage_class=Storage):
|
|
208
|
+
"""
|
|
209
|
+
Open or create persistent storage with a unique `name` and optional
|
|
210
|
+
`storage_class` (used to extend the default `Storage` based behavior).
|
|
211
|
+
|
|
212
|
+
Each storage is isolated by name within the current origin (domain).
|
|
213
|
+
If the storage doesn't exist, it will be created. If it does exist,
|
|
214
|
+
its current contents will be loaded.
|
|
215
|
+
|
|
216
|
+
This function returns a `Storage` instance (or custom subclass instance)
|
|
217
|
+
acting as a persistent dictionary. A `ValueError` is raised if `name` is
|
|
218
|
+
empty or not provided.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pyscript import storage
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Basic usage.
|
|
225
|
+
user_data = await storage("user-profile")
|
|
226
|
+
user_data["name"] = "Alice"
|
|
227
|
+
user_data["age"] = 30
|
|
228
|
+
|
|
229
|
+
# Multiple independent storages.
|
|
230
|
+
settings = await storage("app-settings")
|
|
231
|
+
cache = await storage("api-cache")
|
|
232
|
+
|
|
233
|
+
# With custom Storage class.
|
|
234
|
+
class ValidatingStorage(Storage):
|
|
235
|
+
def __setitem__(self, key, value):
|
|
236
|
+
if not isinstance(key, str):
|
|
237
|
+
raise TypeError("Keys must be strings")
|
|
238
|
+
super().__setitem__(key, value)
|
|
239
|
+
|
|
240
|
+
validated = await storage("validated-data", ValidatingStorage)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Storage names are automatically prefixed with `"@pyscript/"` to
|
|
244
|
+
namespace them within IndexedDB.
|
|
245
|
+
"""
|
|
60
246
|
if not name:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
247
|
+
raise ValueError("Storage name must be a non-empty string")
|
|
248
|
+
|
|
249
|
+
underlying_store = await _polyscript_storage(f"@pyscript/{name}")
|
|
250
|
+
return storage_class(underlying_store)
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains general-purpose utility functions that don't fit into
|
|
3
|
+
more specific modules. These utilities handle cross-platform compatibility
|
|
4
|
+
between Pyodide and MicroPython, feature detection, and common type
|
|
5
|
+
conversions:
|
|
6
|
+
|
|
7
|
+
- `as_bytearray`: Convert JavaScript `ArrayBuffer` to Python `bytearray`.
|
|
8
|
+
- `NotSupported`: Placeholder for unavailable features in specific contexts.
|
|
9
|
+
- `is_awaitable`: Detect `async` functions across Python implementations.
|
|
10
|
+
|
|
11
|
+
These utilities are primarily used internally by PyScript but are available
|
|
12
|
+
for use in application code when needed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
1
15
|
import js
|
|
2
|
-
import sys
|
|
3
16
|
import inspect
|
|
4
17
|
|
|
5
18
|
|
|
6
19
|
def as_bytearray(buffer):
|
|
7
20
|
"""
|
|
8
|
-
Given a JavaScript ArrayBuffer
|
|
21
|
+
Given a JavaScript `ArrayBuffer`, convert it to a Python `bytearray` in a
|
|
9
22
|
MicroPython friendly manner.
|
|
10
23
|
"""
|
|
11
24
|
ui8a = js.Uint8Array.new(buffer)
|
|
@@ -42,17 +55,25 @@ class NotSupported:
|
|
|
42
55
|
def is_awaitable(obj):
|
|
43
56
|
"""
|
|
44
57
|
Returns a boolean indication if the passed in obj is an awaitable
|
|
45
|
-
function.
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
function. This is interpreter agnostic.
|
|
59
|
+
|
|
60
|
+
!!! info
|
|
61
|
+
MicroPython treats awaitables as generator functions, and if
|
|
62
|
+
the object is a closure containing an async function or a bound method
|
|
63
|
+
we need to work carefully.
|
|
48
64
|
"""
|
|
49
65
|
from pyscript import config
|
|
50
66
|
|
|
51
|
-
if config["type"] == "mpy":
|
|
67
|
+
if config["type"] == "mpy":
|
|
52
68
|
# MicroPython doesn't appear to have a way to determine if a closure is
|
|
53
69
|
# an async function except via the repr. This is a bit hacky.
|
|
54
|
-
|
|
70
|
+
r = repr(obj)
|
|
71
|
+
if "<closure <generator>" in r:
|
|
72
|
+
return True
|
|
73
|
+
# Same applies to bound methods.
|
|
74
|
+
if "<bound_method" in r and "<generator>" in r:
|
|
55
75
|
return True
|
|
76
|
+
# In MicroPython, generator functions are awaitable.
|
|
56
77
|
return inspect.isgeneratorfunction(obj)
|
|
57
78
|
|
|
58
79
|
return inspect.iscoroutinefunction(obj)
|