@pyscript/core 0.7.10 → 0.7.12
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-7vXPINKi.js → codemirror-WbVPJuAs.js} +2 -2
- package/dist/codemirror-WbVPJuAs.js.map +1 -0
- package/dist/{codemirror_commands-CN4gxvZk.js → codemirror_commands-BRu2f-2p.js} +2 -2
- package/dist/codemirror_commands-BRu2f-2p.js.map +1 -0
- package/dist/codemirror_lang-python-q-wuh0nL.js +2 -0
- package/dist/codemirror_lang-python-q-wuh0nL.js.map +1 -0
- package/dist/codemirror_language-DqPeLcFN.js +2 -0
- package/dist/codemirror_language-DqPeLcFN.js.map +1 -0
- package/dist/{codemirror_state-BIAL8JKm.js → codemirror_state-DWQh5Ruf.js} +2 -2
- package/dist/codemirror_state-DWQh5Ruf.js.map +1 -0
- package/dist/codemirror_view-CMXZSWgf.js +2 -0
- package/dist/codemirror_view-CMXZSWgf.js.map +1 -0
- package/dist/core-DmpFMpAn.js +4 -0
- package/dist/core-DmpFMpAn.js.map +1 -0
- package/dist/core.js +1 -1
- package/dist/{deprecations-manager-qW023Rjf.js → deprecations-manager-HQLYNCYn.js} +2 -2
- package/dist/{deprecations-manager-qW023Rjf.js.map → deprecations-manager-HQLYNCYn.js.map} +1 -1
- package/dist/{donkey-7fH6-q0I.js → donkey-B3F8VdSV.js} +2 -2
- package/dist/{donkey-7fH6-q0I.js.map → donkey-B3F8VdSV.js.map} +1 -1
- package/dist/{error-CO7OuWCh.js → error-DS_5_C5_.js} +2 -2
- package/dist/{error-CO7OuWCh.js.map → error-DS_5_C5_.js.map} +1 -1
- package/dist/index-DaYI1YXo.js +2 -0
- package/dist/index-DaYI1YXo.js.map +1 -0
- package/dist/{mpy-BZxQ23WL.js → mpy-Bo2uW6nt.js} +2 -2
- package/dist/{mpy-BZxQ23WL.js.map → mpy-Bo2uW6nt.js.map} +1 -1
- package/dist/{py-DI_TP8Id.js → py-BEf8j7L5.js} +2 -2
- package/dist/{py-DI_TP8Id.js.map → py-BEf8j7L5.js.map} +1 -1
- package/dist/{py-editor-BJBbMtNv.js → py-editor-C0XF2rwE.js} +2 -2
- package/dist/{py-editor-BJBbMtNv.js.map → py-editor-C0XF2rwE.js.map} +1 -1
- package/dist/{py-game-C7N5JK0D.js → py-game-eWNz96mt.js} +2 -2
- package/dist/{py-game-C7N5JK0D.js.map → py-game-eWNz96mt.js.map} +1 -1
- package/dist/{py-terminal-Cy8siD6F.js → py-terminal-VtavPj1S.js} +2 -2
- package/dist/{py-terminal-Cy8siD6F.js.map → py-terminal-VtavPj1S.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 +16 -15
- 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 +207 -50
- 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/src/sync.js +1 -1
- package/types/stdlib/pyscript.d.ts +1 -1
- package/dist/codemirror-7vXPINKi.js.map +0 -1
- package/dist/codemirror_commands-CN4gxvZk.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.map +0 -1
- package/dist/codemirror_view-Bt4sLgyA.js +0 -2
- package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
- package/dist/core-5ORB_Mcj.js +0 -4
- package/dist/core-5ORB_Mcj.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-CakRHzZu.js +0 -2
- package/dist/zip-CakRHzZu.js.map +0 -1
- package/src/stdlib/pyscript/magic_js.py +0 -84
|
@@ -1,17 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event handling for PyScript.
|
|
3
|
+
|
|
4
|
+
This module provides two complementary systems:
|
|
5
|
+
|
|
6
|
+
1. The `Event` class: A simple publish-subscribe pattern for custom events
|
|
7
|
+
within *your* Python code.
|
|
8
|
+
|
|
9
|
+
2. The `@when` decorator: Connects Python functions to browser DOM events,
|
|
10
|
+
or instances of the `Event` class, allowing you to respond to user
|
|
11
|
+
interactions like clicks, key presses and form submissions, or to custom
|
|
12
|
+
events defined in your Python code.
|
|
13
|
+
"""
|
|
14
|
+
|
|
1
15
|
import asyncio
|
|
2
16
|
import inspect
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
17
|
from functools import wraps
|
|
6
|
-
from pyscript.
|
|
7
|
-
from pyscript.ffi import create_proxy
|
|
18
|
+
from pyscript.context import document
|
|
19
|
+
from pyscript.ffi import create_proxy, to_js
|
|
8
20
|
from pyscript.util import is_awaitable
|
|
9
|
-
from pyscript import config
|
|
10
21
|
|
|
11
22
|
|
|
12
23
|
class Event:
|
|
13
24
|
"""
|
|
14
|
-
|
|
25
|
+
A custom event that can notify multiple listeners when triggered.
|
|
26
|
+
|
|
27
|
+
Use this class to create your own event system within Python code.
|
|
28
|
+
Listeners can be either regular functions or async functions.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from pyscript.events import Event
|
|
32
|
+
|
|
33
|
+
# Create a custom event.
|
|
34
|
+
data_loaded = Event()
|
|
35
|
+
|
|
36
|
+
# Add a listener.
|
|
37
|
+
def on_data_loaded(result):
|
|
38
|
+
print(f"Data loaded: {result}")
|
|
39
|
+
|
|
40
|
+
data_loaded.add_listener(on_data_loaded)
|
|
41
|
+
|
|
42
|
+
# Time passes.... trigger the event.
|
|
43
|
+
data_loaded.trigger({"data": 123})
|
|
44
|
+
```
|
|
15
45
|
"""
|
|
16
46
|
|
|
17
47
|
def __init__(self):
|
|
@@ -19,116 +49,189 @@ class Event:
|
|
|
19
49
|
|
|
20
50
|
def trigger(self, result):
|
|
21
51
|
"""
|
|
22
|
-
Trigger the event
|
|
52
|
+
Trigger the event and notify all listeners with the given `result`.
|
|
23
53
|
"""
|
|
24
54
|
for listener in self._listeners:
|
|
25
55
|
if is_awaitable(listener):
|
|
26
|
-
# Use create task to avoid making this an async function.
|
|
27
56
|
asyncio.create_task(listener(result))
|
|
28
57
|
else:
|
|
29
58
|
listener(result)
|
|
30
59
|
|
|
31
60
|
def add_listener(self, listener):
|
|
32
61
|
"""
|
|
33
|
-
Add a
|
|
62
|
+
Add a function to be called when this event is triggered.
|
|
63
|
+
|
|
64
|
+
The `listener` must be callable. It can be either a regular function
|
|
65
|
+
or an async function. Duplicate listeners are ignored.
|
|
34
66
|
"""
|
|
35
|
-
if
|
|
36
|
-
|
|
37
|
-
self._listeners.append(listener)
|
|
38
|
-
else:
|
|
39
|
-
msg = "Listener must be callable or awaitable."
|
|
67
|
+
if not callable(listener):
|
|
68
|
+
msg = "Listener must be callable."
|
|
40
69
|
raise ValueError(msg)
|
|
70
|
+
if listener not in self._listeners:
|
|
71
|
+
self._listeners.append(listener)
|
|
41
72
|
|
|
42
|
-
def remove_listener(self, *
|
|
73
|
+
def remove_listener(self, *listeners):
|
|
43
74
|
"""
|
|
44
|
-
|
|
45
|
-
provided, clear all handlers.
|
|
75
|
+
Remove specified `listeners`. If none specified, remove all listeners.
|
|
46
76
|
"""
|
|
47
|
-
if
|
|
48
|
-
for listener in
|
|
49
|
-
|
|
77
|
+
if listeners:
|
|
78
|
+
for listener in listeners:
|
|
79
|
+
try:
|
|
80
|
+
self._listeners.remove(listener)
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass # Silently ignore listeners not in the list.
|
|
50
83
|
else:
|
|
51
84
|
self._listeners = []
|
|
52
85
|
|
|
53
86
|
|
|
54
|
-
def when(
|
|
87
|
+
def when(event_type, selector=None, **options):
|
|
55
88
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
A decorator to handle DOM events or custom `Event` objects.
|
|
90
|
+
|
|
91
|
+
For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
|
|
92
|
+
for target elements. For custom `Event` objects, just pass the `Event`
|
|
93
|
+
instance as the `event_type`. It's also possible to pass a list of `Event`
|
|
94
|
+
objects. The `selector` is required only for DOM events. It should be a
|
|
95
|
+
[CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
|
|
96
|
+
`Element`, `ElementCollection`, or list of DOM elements.
|
|
97
|
+
|
|
98
|
+
For DOM events only, you can specify optional
|
|
99
|
+
[addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options):
|
|
100
|
+
`capture`, `once`, `passive`, or `signal`.
|
|
101
|
+
|
|
102
|
+
The decorated function can be either a regular function or an async
|
|
103
|
+
function. If the function accepts an argument, it will receive the event
|
|
104
|
+
object (for DOM events) or the Event's result (for custom events). A
|
|
105
|
+
function does not need to accept any arguments if it doesn't require them.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pyscript import when, display
|
|
109
|
+
|
|
110
|
+
# Handle DOM events.
|
|
111
|
+
@when("click", "#my-button")
|
|
112
|
+
def handle_click(event):
|
|
113
|
+
display("Button clicked!")
|
|
114
|
+
|
|
115
|
+
# Handle DOM events with options.
|
|
116
|
+
@when("click", "#my-button", once=True)
|
|
117
|
+
def handle_click_once(event):
|
|
118
|
+
display("Button clicked once!")
|
|
119
|
+
|
|
120
|
+
# Handle custom events.
|
|
121
|
+
my_event = Event()
|
|
122
|
+
|
|
123
|
+
@when(my_event)
|
|
124
|
+
def handle_custom(): # No event argument needed.
|
|
125
|
+
display("Custom event triggered!")
|
|
126
|
+
|
|
127
|
+
# Handle multiple custom events.
|
|
128
|
+
another_event = Event()
|
|
129
|
+
|
|
130
|
+
def another_handler():
|
|
131
|
+
display("Another custom event handler.")
|
|
132
|
+
|
|
133
|
+
# Attach the same handler to multiple events but not as a decorator.
|
|
134
|
+
when([my_event, another_event])(another_handler)
|
|
135
|
+
|
|
136
|
+
# Trigger an Event instance from a DOM event via @when.
|
|
137
|
+
@when("click", "#my-button")
|
|
138
|
+
def handle_click(event):
|
|
139
|
+
another_event.trigger("Button clicked!")
|
|
140
|
+
|
|
141
|
+
# Stacked decorators also work.
|
|
142
|
+
@when("mouseover", "#my-div")
|
|
143
|
+
@when(my_event)
|
|
144
|
+
def handle_both(event):
|
|
145
|
+
display("Either mouseover or custom event triggered!")
|
|
146
|
+
```
|
|
66
147
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
handler = None
|
|
70
|
-
if args and (callable(args[0]) or is_awaitable(args[0])):
|
|
71
|
-
handler = args[0]
|
|
72
|
-
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
|
73
|
-
handler = kwargs.pop("handler")
|
|
74
|
-
# If the target is a string, it is the "older" use of `when` where it
|
|
75
|
-
# represents the name of a DOM event.
|
|
76
|
-
if isinstance(target, str):
|
|
77
|
-
# Extract the selector from the arguments or keyword arguments.
|
|
78
|
-
selector = args[0] if args else kwargs.pop("selector")
|
|
148
|
+
if isinstance(event_type, str):
|
|
149
|
+
# This is a DOM event to handle, so check and use the selector.
|
|
79
150
|
if not selector:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if isinstance(selector, str):
|
|
86
|
-
elements = document.querySelectorAll(selector)
|
|
87
|
-
elif isinstance(selector, Element):
|
|
88
|
-
elements = [selector._dom_element]
|
|
89
|
-
elif isinstance(selector, ElementCollection):
|
|
90
|
-
elements = [el._dom_element for el in selector]
|
|
91
|
-
else:
|
|
92
|
-
elements = selector if isinstance(selector, list) else [selector]
|
|
151
|
+
raise ValueError("Selector required for DOM event handling.")
|
|
152
|
+
elements = _get_elements(selector)
|
|
153
|
+
if not elements:
|
|
154
|
+
raise ValueError(f"No elements found for selector: {selector}")
|
|
93
155
|
|
|
94
156
|
def decorator(func):
|
|
95
|
-
|
|
96
|
-
if
|
|
97
|
-
|
|
157
|
+
wrapper = _create_wrapper(func)
|
|
158
|
+
if isinstance(event_type, Event):
|
|
159
|
+
# Custom Event - add listener.
|
|
160
|
+
event_type.add_listener(wrapper)
|
|
161
|
+
elif isinstance(event_type, list) and all(
|
|
162
|
+
isinstance(t, Event) for t in event_type
|
|
163
|
+
):
|
|
164
|
+
# List of custom Events - add listener to each.
|
|
165
|
+
for event in event_type:
|
|
166
|
+
event.add_listener(wrapper)
|
|
167
|
+
else:
|
|
168
|
+
# DOM event - attach to all matched elements.
|
|
169
|
+
for element in elements:
|
|
170
|
+
element.addEventListener(
|
|
171
|
+
event_type,
|
|
172
|
+
create_proxy(wrapper),
|
|
173
|
+
to_js(options) if options else False,
|
|
174
|
+
)
|
|
175
|
+
return wrapper
|
|
98
176
|
|
|
99
|
-
|
|
100
|
-
|
|
177
|
+
return decorator
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _get_elements(selector):
|
|
181
|
+
"""
|
|
182
|
+
Convert various `selector` types into a list of DOM elements.
|
|
183
|
+
"""
|
|
184
|
+
from pyscript.web import Element, ElementCollection
|
|
185
|
+
|
|
186
|
+
if isinstance(selector, str):
|
|
187
|
+
return list(document.querySelectorAll(selector))
|
|
188
|
+
elif isinstance(selector, Element):
|
|
189
|
+
return [selector._dom_element]
|
|
190
|
+
elif isinstance(selector, ElementCollection):
|
|
191
|
+
return [el._dom_element for el in selector]
|
|
192
|
+
elif isinstance(selector, list):
|
|
193
|
+
return selector
|
|
194
|
+
else:
|
|
195
|
+
return [selector]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _create_wrapper(func):
|
|
199
|
+
"""
|
|
200
|
+
Create an appropriate wrapper for the given function, `func`.
|
|
201
|
+
|
|
202
|
+
The wrapper handles both sync and async functions, and respects whether
|
|
203
|
+
the function expects to receive event arguments.
|
|
204
|
+
"""
|
|
205
|
+
# Get the original function if it's been wrapped. This avoids wrapper
|
|
206
|
+
# loops when stacking decorators.
|
|
207
|
+
original_func = func
|
|
208
|
+
while hasattr(original_func, "__wrapped__"):
|
|
209
|
+
original_func = original_func.__wrapped__
|
|
210
|
+
# Inspect the original function signature.
|
|
211
|
+
sig = inspect.signature(original_func)
|
|
212
|
+
accepts_args = bool(sig.parameters)
|
|
213
|
+
if is_awaitable(func):
|
|
214
|
+
if accepts_args:
|
|
215
|
+
|
|
216
|
+
async def wrapper(event):
|
|
217
|
+
return await func(event)
|
|
101
218
|
|
|
102
|
-
else:
|
|
103
|
-
wrapper = func
|
|
104
219
|
else:
|
|
105
|
-
# Function doesn't receive events.
|
|
106
|
-
if is_awaitable(func):
|
|
107
220
|
|
|
108
|
-
|
|
109
|
-
|
|
221
|
+
async def wrapper(*args, **kwargs):
|
|
222
|
+
return await func()
|
|
110
223
|
|
|
111
|
-
|
|
224
|
+
else:
|
|
225
|
+
if accepts_args:
|
|
226
|
+
# Always create a new wrapper function to avoid issues with
|
|
227
|
+
# stacked decorators getting into an infinite loop.
|
|
228
|
+
|
|
229
|
+
def wrapper(event):
|
|
230
|
+
return func(event)
|
|
112
231
|
|
|
113
|
-
def wrapper(*args, **kwargs):
|
|
114
|
-
return func()
|
|
115
|
-
|
|
116
|
-
wrapper = wraps(func)(wrapper)
|
|
117
|
-
if isinstance(target, Event):
|
|
118
|
-
# The target is a single Event object.
|
|
119
|
-
target.add_listener(wrapper)
|
|
120
|
-
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
|
121
|
-
# The target is a list of Event objects.
|
|
122
|
-
for evt in target:
|
|
123
|
-
evt.add_listener(wrapper)
|
|
124
232
|
else:
|
|
125
|
-
# The target is a string representing an event type, and so a
|
|
126
|
-
# DOM element or collection of elements is found in "elements".
|
|
127
|
-
for el in elements:
|
|
128
|
-
el.addEventListener(target, create_proxy(wrapper))
|
|
129
|
-
return wrapper
|
|
130
233
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
234
|
+
def wrapper(*args, **kwargs):
|
|
235
|
+
return func()
|
|
236
|
+
|
|
237
|
+
return wraps(func)(wrapper)
|
|
@@ -1,87 +1,218 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module provides a Python-friendly interface to the
|
|
3
|
+
[browser's fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
|
|
4
|
+
returning native Python data types and supporting directly awaiting the promise
|
|
5
|
+
and chaining method calls directly on the promise.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pyscript.fetch import fetch
|
|
9
|
+
url = "https://api.example.com/data"
|
|
10
|
+
|
|
11
|
+
# Pattern 1: Await the response, then extract data.
|
|
12
|
+
response = await fetch(url)
|
|
13
|
+
if response.ok:
|
|
14
|
+
data = await response.json()
|
|
15
|
+
else:
|
|
16
|
+
raise NetworkError(f"Fetch failed: {response.status}")
|
|
17
|
+
|
|
18
|
+
# Pattern 2: Chain method calls directly on the promise.
|
|
19
|
+
data = await fetch(url).json()
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
2
22
|
|
|
23
|
+
import json
|
|
3
24
|
import js
|
|
4
25
|
from pyscript.util import as_bytearray
|
|
5
26
|
|
|
6
27
|
|
|
7
|
-
|
|
8
|
-
|
|
28
|
+
class _FetchResponse:
|
|
29
|
+
"""
|
|
30
|
+
Wraps a JavaScript Response object with Pythonic data extraction methods.
|
|
31
|
+
|
|
32
|
+
This wrapper ensures that data returned from fetch is, if possible, in
|
|
33
|
+
native Python types rather than JavaScript types.
|
|
34
|
+
"""
|
|
35
|
+
|
|
9
36
|
def __init__(self, response):
|
|
10
37
|
self._response = response
|
|
11
38
|
|
|
12
|
-
# grant access to response.ok and other fields
|
|
13
39
|
def __getattr__(self, attr):
|
|
40
|
+
"""
|
|
41
|
+
Provide access to underlying Response properties like ok, status, etc.
|
|
42
|
+
"""
|
|
14
43
|
return getattr(self._response, attr)
|
|
15
44
|
|
|
16
|
-
# exposed methods with Pythonic results
|
|
17
45
|
async def arrayBuffer(self):
|
|
46
|
+
"""
|
|
47
|
+
Get response body as a buffer (memoryview or bytes).
|
|
48
|
+
|
|
49
|
+
Returns a memoryview in MicroPython or bytes in Pyodide, representing
|
|
50
|
+
the raw binary data.
|
|
51
|
+
"""
|
|
18
52
|
buffer = await self._response.arrayBuffer()
|
|
19
|
-
# works in Pyodide
|
|
20
53
|
if hasattr(buffer, "to_py"):
|
|
54
|
+
# Pyodide conversion.
|
|
21
55
|
return buffer.to_py()
|
|
22
|
-
#
|
|
56
|
+
# MicroPython conversion.
|
|
23
57
|
return memoryview(as_bytearray(buffer))
|
|
24
58
|
|
|
25
59
|
async def blob(self):
|
|
60
|
+
"""
|
|
61
|
+
Get response body as a JavaScript Blob object.
|
|
62
|
+
|
|
63
|
+
Returns the raw JS Blob for use with other JS APIs.
|
|
64
|
+
"""
|
|
26
65
|
return await self._response.blob()
|
|
27
66
|
|
|
28
67
|
async def bytearray(self):
|
|
68
|
+
"""
|
|
69
|
+
Get response body as a Python bytearray.
|
|
70
|
+
|
|
71
|
+
Returns a mutable bytearray containing the response data.
|
|
72
|
+
"""
|
|
29
73
|
buffer = await self._response.arrayBuffer()
|
|
30
74
|
return as_bytearray(buffer)
|
|
31
75
|
|
|
32
76
|
async def json(self):
|
|
77
|
+
"""
|
|
78
|
+
Parse response body as JSON and return Python objects.
|
|
79
|
+
|
|
80
|
+
Returns native Python dicts, lists, strings, numbers, etc.
|
|
81
|
+
"""
|
|
33
82
|
return json.loads(await self.text())
|
|
34
83
|
|
|
35
84
|
async def text(self):
|
|
85
|
+
"""
|
|
86
|
+
Get response body as a text string.
|
|
87
|
+
"""
|
|
36
88
|
return await self._response.text()
|
|
37
89
|
|
|
38
90
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
91
|
+
class _FetchPromise:
|
|
92
|
+
"""
|
|
93
|
+
Wraps the fetch promise to enable direct method chaining.
|
|
94
|
+
|
|
95
|
+
This allows calling response methods directly on the fetch promise:
|
|
96
|
+
`await fetch(url).json()` instead of requiring two separate awaits.
|
|
97
|
+
|
|
98
|
+
This feels more Pythonic since it matches typical usage patterns
|
|
99
|
+
Python developers have got used to via libraries like `requests`.
|
|
100
|
+
"""
|
|
45
101
|
|
|
46
102
|
def __init__(self, promise):
|
|
47
103
|
self._promise = promise
|
|
104
|
+
# To be resolved in the future via the setup() static method.
|
|
48
105
|
promise._response = None
|
|
106
|
+
# Add convenience methods directly to the promise.
|
|
49
107
|
promise.arrayBuffer = self.arrayBuffer
|
|
50
108
|
promise.blob = self.blob
|
|
51
109
|
promise.bytearray = self.bytearray
|
|
52
110
|
promise.json = self.json
|
|
53
111
|
promise.text = self.text
|
|
54
112
|
|
|
55
|
-
|
|
113
|
+
@staticmethod
|
|
114
|
+
def setup(promise, response):
|
|
115
|
+
"""
|
|
116
|
+
Store the resolved response on the promise for later access.
|
|
117
|
+
"""
|
|
118
|
+
promise._response = _FetchResponse(response)
|
|
119
|
+
return promise._response
|
|
120
|
+
|
|
121
|
+
async def _get_response(self):
|
|
122
|
+
"""
|
|
123
|
+
Get the cached response, or await the promise if not yet resolved.
|
|
124
|
+
"""
|
|
56
125
|
if not self._promise._response:
|
|
57
126
|
await self._promise
|
|
58
127
|
return self._promise._response
|
|
59
128
|
|
|
60
129
|
async def arrayBuffer(self):
|
|
61
|
-
response = await self.
|
|
130
|
+
response = await self._get_response()
|
|
62
131
|
return await response.arrayBuffer()
|
|
63
132
|
|
|
64
133
|
async def blob(self):
|
|
65
|
-
response = await self.
|
|
134
|
+
response = await self._get_response()
|
|
66
135
|
return await response.blob()
|
|
67
136
|
|
|
68
137
|
async def bytearray(self):
|
|
69
|
-
response = await self.
|
|
138
|
+
response = await self._get_response()
|
|
70
139
|
return await response.bytearray()
|
|
71
140
|
|
|
72
141
|
async def json(self):
|
|
73
|
-
response = await self.
|
|
142
|
+
response = await self._get_response()
|
|
74
143
|
return await response.json()
|
|
75
144
|
|
|
76
145
|
async def text(self):
|
|
77
|
-
response = await self.
|
|
146
|
+
response = await self._get_response()
|
|
78
147
|
return await response.text()
|
|
79
148
|
|
|
80
149
|
|
|
81
|
-
def fetch(url, **
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
def fetch(url, **options):
|
|
151
|
+
"""
|
|
152
|
+
Fetch a resource from the network using a Pythonic interface.
|
|
153
|
+
|
|
154
|
+
This wraps JavaScript's fetch API, returning Python-native data types
|
|
155
|
+
and supporting both direct promise awaiting and method chaining.
|
|
156
|
+
|
|
157
|
+
The function takes a `url` and optional fetch `options` as keyword
|
|
158
|
+
arguments. The `options` correspond to the JavaScript fetch API's
|
|
159
|
+
[RequestInit dictionary](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit),
|
|
160
|
+
and commonly include:
|
|
161
|
+
|
|
162
|
+
- `method`: HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"` etc.)
|
|
163
|
+
- `headers`: Dict of request headers.
|
|
164
|
+
- `body`: Request body (string, dict for JSON, etc.)
|
|
165
|
+
|
|
166
|
+
The function returns a promise that resolves to a Response-like object
|
|
167
|
+
with Pythonic methods to extract data:
|
|
168
|
+
|
|
169
|
+
- `await response.json()` to get JSON as Python objects.
|
|
170
|
+
- `await response.text()` to get text data.
|
|
171
|
+
- `await response.bytearray()` to get raw data as a bytearray.
|
|
172
|
+
- `await response.arrayBuffer()` to get raw data as a memoryview or bytes.
|
|
173
|
+
- `await response.blob()` to get the raw JS Blob object.
|
|
174
|
+
|
|
175
|
+
It's also possible to chain these methods directly on the fetch promise:
|
|
176
|
+
`data = await fetch(url).json()`
|
|
177
|
+
|
|
178
|
+
The returned response object also exposes standard properties like
|
|
179
|
+
`ok`, `status`, and `statusText` for checking response status.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Simple GET request.
|
|
183
|
+
response = await fetch("https://api.example.com/data")
|
|
184
|
+
data = await response.json()
|
|
185
|
+
|
|
186
|
+
# Method chaining.
|
|
187
|
+
data = await fetch("https://api.example.com/data").json()
|
|
188
|
+
|
|
189
|
+
# POST request with JSON.
|
|
190
|
+
response = await fetch(
|
|
191
|
+
"https://api.example.com/users",
|
|
192
|
+
method="POST",
|
|
193
|
+
headers={"Content-Type": "application/json"},
|
|
194
|
+
body=json.dumps({"name": "Alice"})
|
|
195
|
+
)
|
|
196
|
+
result = await response.json()
|
|
197
|
+
|
|
198
|
+
# Check response status codes.
|
|
199
|
+
response = await fetch("https://api.example.com/data")
|
|
200
|
+
if response.ok:
|
|
201
|
+
# Status in the range 200-299.
|
|
202
|
+
data = await response.json()
|
|
203
|
+
elif response.status == 404:
|
|
204
|
+
print("Resource not found")
|
|
205
|
+
else:
|
|
206
|
+
print(f"Error: {response.status} {response.statusText}")
|
|
207
|
+
```
|
|
208
|
+
"""
|
|
209
|
+
# Convert Python dict to JavaScript object.
|
|
210
|
+
js_options = js.JSON.parse(json.dumps(options))
|
|
211
|
+
|
|
212
|
+
# Setup response handler to wrap the result.
|
|
213
|
+
def on_response(response, *_):
|
|
214
|
+
return _FetchPromise.setup(promise, response)
|
|
215
|
+
|
|
216
|
+
promise = js.fetch(url, js_options).then(on_response)
|
|
217
|
+
_FetchPromise(promise)
|
|
87
218
|
return promise
|