@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.
Files changed (80) hide show
  1. package/dist/{codemirror-7vXPINKi.js → codemirror-WbVPJuAs.js} +2 -2
  2. package/dist/codemirror-WbVPJuAs.js.map +1 -0
  3. package/dist/{codemirror_commands-CN4gxvZk.js → codemirror_commands-BRu2f-2p.js} +2 -2
  4. package/dist/codemirror_commands-BRu2f-2p.js.map +1 -0
  5. package/dist/codemirror_lang-python-q-wuh0nL.js +2 -0
  6. package/dist/codemirror_lang-python-q-wuh0nL.js.map +1 -0
  7. package/dist/codemirror_language-DqPeLcFN.js +2 -0
  8. package/dist/codemirror_language-DqPeLcFN.js.map +1 -0
  9. package/dist/{codemirror_state-BIAL8JKm.js → codemirror_state-DWQh5Ruf.js} +2 -2
  10. package/dist/codemirror_state-DWQh5Ruf.js.map +1 -0
  11. package/dist/codemirror_view-CMXZSWgf.js +2 -0
  12. package/dist/codemirror_view-CMXZSWgf.js.map +1 -0
  13. package/dist/core-DmpFMpAn.js +4 -0
  14. package/dist/core-DmpFMpAn.js.map +1 -0
  15. package/dist/core.js +1 -1
  16. package/dist/{deprecations-manager-qW023Rjf.js → deprecations-manager-HQLYNCYn.js} +2 -2
  17. package/dist/{deprecations-manager-qW023Rjf.js.map → deprecations-manager-HQLYNCYn.js.map} +1 -1
  18. package/dist/{donkey-7fH6-q0I.js → donkey-B3F8VdSV.js} +2 -2
  19. package/dist/{donkey-7fH6-q0I.js.map → donkey-B3F8VdSV.js.map} +1 -1
  20. package/dist/{error-CO7OuWCh.js → error-DS_5_C5_.js} +2 -2
  21. package/dist/{error-CO7OuWCh.js.map → error-DS_5_C5_.js.map} +1 -1
  22. package/dist/index-DaYI1YXo.js +2 -0
  23. package/dist/index-DaYI1YXo.js.map +1 -0
  24. package/dist/{mpy-BZxQ23WL.js → mpy-Bo2uW6nt.js} +2 -2
  25. package/dist/{mpy-BZxQ23WL.js.map → mpy-Bo2uW6nt.js.map} +1 -1
  26. package/dist/{py-DI_TP8Id.js → py-BEf8j7L5.js} +2 -2
  27. package/dist/{py-DI_TP8Id.js.map → py-BEf8j7L5.js.map} +1 -1
  28. package/dist/{py-editor-BJBbMtNv.js → py-editor-C0XF2rwE.js} +2 -2
  29. package/dist/{py-editor-BJBbMtNv.js.map → py-editor-C0XF2rwE.js.map} +1 -1
  30. package/dist/{py-game-C7N5JK0D.js → py-game-eWNz96mt.js} +2 -2
  31. package/dist/{py-game-C7N5JK0D.js.map → py-game-eWNz96mt.js.map} +1 -1
  32. package/dist/{py-terminal-Cy8siD6F.js → py-terminal-VtavPj1S.js} +2 -2
  33. package/dist/{py-terminal-Cy8siD6F.js.map → py-terminal-VtavPj1S.js.map} +1 -1
  34. package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
  35. package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
  36. package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
  37. package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
  38. package/dist/zip-CgZGjqjF.js +2 -0
  39. package/dist/zip-CgZGjqjF.js.map +1 -0
  40. package/package.json +16 -15
  41. package/src/3rd-party/xterm_addon-fit.js +14 -2
  42. package/src/3rd-party/xterm_addon-web-links.js +14 -2
  43. package/src/core.js +13 -2
  44. package/src/stdlib/pyscript/__init__.py +100 -31
  45. package/src/stdlib/pyscript/context.py +198 -0
  46. package/src/stdlib/pyscript/display.py +211 -127
  47. package/src/stdlib/pyscript/events.py +191 -88
  48. package/src/stdlib/pyscript/fetch.py +156 -25
  49. package/src/stdlib/pyscript/ffi.py +132 -16
  50. package/src/stdlib/pyscript/flatted.py +78 -1
  51. package/src/stdlib/pyscript/fs.py +207 -50
  52. package/src/stdlib/pyscript/media.py +210 -50
  53. package/src/stdlib/pyscript/storage.py +214 -27
  54. package/src/stdlib/pyscript/util.py +28 -7
  55. package/src/stdlib/pyscript/web.py +1079 -881
  56. package/src/stdlib/pyscript/websocket.py +252 -45
  57. package/src/stdlib/pyscript/workers.py +176 -27
  58. package/src/stdlib/pyscript.js +13 -13
  59. package/src/sync.js +1 -1
  60. package/types/stdlib/pyscript.d.ts +1 -1
  61. package/dist/codemirror-7vXPINKi.js.map +0 -1
  62. package/dist/codemirror_commands-CN4gxvZk.js.map +0 -1
  63. package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
  64. package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
  65. package/dist/codemirror_language-DOkvasqm.js +0 -2
  66. package/dist/codemirror_language-DOkvasqm.js.map +0 -1
  67. package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
  68. package/dist/codemirror_view-Bt4sLgyA.js +0 -2
  69. package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
  70. package/dist/core-5ORB_Mcj.js +0 -4
  71. package/dist/core-5ORB_Mcj.js.map +0 -1
  72. package/dist/index-jZ1aOVVJ.js +0 -2
  73. package/dist/index-jZ1aOVVJ.js.map +0 -1
  74. package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
  75. package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
  76. package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
  77. package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
  78. package/dist/zip-CakRHzZu.js +0 -2
  79. package/dist/zip-CakRHzZu.js.map +0 -1
  80. 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.magic_js import document
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
- Represents something that may happen at some point in the future.
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 with a result to pass into the handlers.
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 callable/awaitable to listen to when this event is triggered.
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 is_awaitable(listener) or callable(listener):
36
- if listener not in self._listeners:
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, *args):
73
+ def remove_listener(self, *listeners):
43
74
  """
44
- Clear the specified handler functions in *args. If no handlers
45
- provided, clear all handlers.
75
+ Remove specified `listeners`. If none specified, remove all listeners.
46
76
  """
47
- if args:
48
- for listener in args:
49
- self._listeners.remove(listener)
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(target, *args, **kwargs):
87
+ def when(event_type, selector=None, **options):
55
88
  """
56
- Add an event listener to the target element(s) for the specified event type.
57
-
58
- The target can be a string representing the event type, or an Event object.
59
- If the target is an Event object, the event listener will be added to that
60
- object. If the target is a string, the event listener will be added to the
61
- element(s) that match the (second) selector argument.
62
-
63
- If a (third) handler argument is provided, it will be called when the event
64
- is triggered; thus allowing this to be used as both a function and a
65
- decorator.
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
- # If "when" is called as a function, try to grab the handler from the
68
- # arguments. If there's no handler, this must be a decorator based call.
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
- msg = "No selector provided."
81
- raise ValueError(msg)
82
- # Grab the DOM elements to which the target event will be attached.
83
- from pyscript.web import Element, ElementCollection
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
- sig = inspect.signature(func)
96
- if sig.parameters:
97
- if is_awaitable(func):
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
- async def wrapper(event):
100
- return await func(event)
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
- async def wrapper(*args, **kwargs):
109
- return await func()
221
+ async def wrapper(*args, **kwargs):
222
+ return await func()
110
223
 
111
- else:
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
- # If "when" was called as a decorator, return the decorator function,
132
- # otherwise just call the internal decorator function with the supplied
133
- # handler.
134
- return decorator(handler) if handler else decorator
234
+ def wrapper(*args, **kwargs):
235
+ return func()
236
+
237
+ return wraps(func)(wrapper)
@@ -1,87 +1,218 @@
1
- import json
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
- ### wrap the response to grant Pythonic results
8
- class _Response:
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
- # shims in MicroPython
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
- ### allow direct await to _Response methods
40
- class _DirectResponse:
41
- @staticmethod
42
- def setup(promise, response):
43
- promise._response = _Response(response)
44
- return promise._response
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
- async def _response(self):
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._response()
130
+ response = await self._get_response()
62
131
  return await response.arrayBuffer()
63
132
 
64
133
  async def blob(self):
65
- response = await self._response()
134
+ response = await self._get_response()
66
135
  return await response.blob()
67
136
 
68
137
  async def bytearray(self):
69
- response = await self._response()
138
+ response = await self._get_response()
70
139
  return await response.bytearray()
71
140
 
72
141
  async def json(self):
73
- response = await self._response()
142
+ response = await self._get_response()
74
143
  return await response.json()
75
144
 
76
145
  async def text(self):
77
- response = await self._response()
146
+ response = await self._get_response()
78
147
  return await response.text()
79
148
 
80
149
 
81
- def fetch(url, **kw):
82
- # workaround Pyodide / MicroPython dict <-> js conversion
83
- options = js.JSON.parse(json.dumps(kw))
84
- awaited = lambda response, *args: _DirectResponse.setup(promise, response)
85
- promise = js.fetch(url, options).then(awaited)
86
- _DirectResponse(promise)
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