mirror of
				https://gitlab.sectorq.eu/jaydee/omv_backup.git
				synced 2025-10-31 10:31:11 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			709 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			709 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| """
 | |
| Support for streaming http requests in emscripten.
 | |
| 
 | |
| A few caveats -
 | |
| 
 | |
| If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled
 | |
| https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md
 | |
| *and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the
 | |
| JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case
 | |
| timeouts and streaming should just work.
 | |
| 
 | |
| Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming.
 | |
| 
 | |
| This approach has several caveats:
 | |
| 
 | |
| Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed.
 | |
| Streaming only works if you're running pyodide in a web worker.
 | |
| 
 | |
| Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch
 | |
| operation, so it requires that you have crossOriginIsolation enabled, by serving over https
 | |
| (or from localhost) with the two headers below set:
 | |
| 
 | |
|     Cross-Origin-Opener-Policy: same-origin
 | |
|     Cross-Origin-Embedder-Policy: require-corp
 | |
| 
 | |
| You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in
 | |
| JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole
 | |
| request into a buffer and then returning it. it shows a warning in the JavaScript console in this case.
 | |
| 
 | |
| Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once
 | |
| control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch.
 | |
| 
 | |
| NB: in this code, there are a lot of JavaScript objects. They are named js_*
 | |
| to make it clear what type of object they are.
 | |
| """
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import io
 | |
| import json
 | |
| from email.parser import Parser
 | |
| from importlib.resources import files
 | |
| from typing import TYPE_CHECKING, Any
 | |
| 
 | |
| import js  # type: ignore[import-not-found]
 | |
| from pyodide.ffi import (  # type: ignore[import-not-found]
 | |
|     JsArray,
 | |
|     JsException,
 | |
|     JsProxy,
 | |
|     to_js,
 | |
| )
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from typing_extensions import Buffer
 | |
| 
 | |
| from .request import EmscriptenRequest
 | |
| from .response import EmscriptenResponse
 | |
| 
 | |
| """
 | |
| There are some headers that trigger unintended CORS preflight requests.
 | |
| See also https://github.com/koenvo/pyodide-http/issues/22
 | |
| """
 | |
| HEADERS_TO_IGNORE = ("user-agent",)
 | |
| 
 | |
| SUCCESS_HEADER = -1
 | |
| SUCCESS_EOF = -2
 | |
| ERROR_TIMEOUT = -3
 | |
| ERROR_EXCEPTION = -4
 | |
| 
 | |
| _STREAMING_WORKER_CODE = (
 | |
|     files(__package__)
 | |
|     .joinpath("emscripten_fetch_worker.js")
 | |
|     .read_text(encoding="utf-8")
 | |
| )
 | |
| 
 | |
| 
 | |
| class _RequestError(Exception):
 | |
|     def __init__(
 | |
|         self,
 | |
|         message: str | None = None,
 | |
|         *,
 | |
|         request: EmscriptenRequest | None = None,
 | |
|         response: EmscriptenResponse | None = None,
 | |
|     ):
 | |
|         self.request = request
 | |
|         self.response = response
 | |
|         self.message = message
 | |
|         super().__init__(self.message)
 | |
| 
 | |
| 
 | |
| class _StreamingError(_RequestError):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class _TimeoutError(_RequestError):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy:
 | |
|     return to_js(dict_val, dict_converter=js.Object.fromEntries)
 | |
| 
 | |
| 
 | |
| class _ReadStream(io.RawIOBase):
 | |
|     def __init__(
 | |
|         self,
 | |
|         int_buffer: JsArray,
 | |
|         byte_buffer: JsArray,
 | |
|         timeout: float,
 | |
|         worker: JsProxy,
 | |
|         connection_id: int,
 | |
|         request: EmscriptenRequest,
 | |
|     ):
 | |
|         self.int_buffer = int_buffer
 | |
|         self.byte_buffer = byte_buffer
 | |
|         self.read_pos = 0
 | |
|         self.read_len = 0
 | |
|         self.connection_id = connection_id
 | |
|         self.worker = worker
 | |
|         self.timeout = int(1000 * timeout) if timeout > 0 else None
 | |
|         self.is_live = True
 | |
|         self._is_closed = False
 | |
|         self.request: EmscriptenRequest | None = request
 | |
| 
 | |
|     def __del__(self) -> None:
 | |
|         self.close()
 | |
| 
 | |
|     # this is compatible with _base_connection
 | |
|     def is_closed(self) -> bool:
 | |
|         return self._is_closed
 | |
| 
 | |
|     # for compatibility with RawIOBase
 | |
|     @property
 | |
|     def closed(self) -> bool:
 | |
|         return self.is_closed()
 | |
| 
 | |
|     def close(self) -> None:
 | |
|         if self.is_closed():
 | |
|             return
 | |
|         self.read_len = 0
 | |
|         self.read_pos = 0
 | |
|         self.int_buffer = None
 | |
|         self.byte_buffer = None
 | |
|         self._is_closed = True
 | |
|         self.request = None
 | |
|         if self.is_live:
 | |
|             self.worker.postMessage(_obj_from_dict({"close": self.connection_id}))
 | |
|             self.is_live = False
 | |
|         super().close()
 | |
| 
 | |
|     def readable(self) -> bool:
 | |
|         return True
 | |
| 
 | |
|     def writable(self) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def seekable(self) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def readinto(self, byte_obj: Buffer) -> int:
 | |
|         if not self.int_buffer:
 | |
|             raise _StreamingError(
 | |
|                 "No buffer for stream in _ReadStream.readinto",
 | |
|                 request=self.request,
 | |
|                 response=None,
 | |
|             )
 | |
|         if self.read_len == 0:
 | |
|             # wait for the worker to send something
 | |
|             js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT)
 | |
|             self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id}))
 | |
|             if (
 | |
|                 js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout)
 | |
|                 == "timed-out"
 | |
|             ):
 | |
|                 raise _TimeoutError
 | |
|             data_len = self.int_buffer[0]
 | |
|             if data_len > 0:
 | |
|                 self.read_len = data_len
 | |
|                 self.read_pos = 0
 | |
|             elif data_len == ERROR_EXCEPTION:
 | |
|                 string_len = self.int_buffer[1]
 | |
|                 # decode the error string
 | |
|                 js_decoder = js.TextDecoder.new()
 | |
|                 json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len))
 | |
|                 raise _StreamingError(
 | |
|                     f"Exception thrown in fetch: {json_str}",
 | |
|                     request=self.request,
 | |
|                     response=None,
 | |
|                 )
 | |
|             else:
 | |
|                 # EOF, free the buffers and return zero
 | |
|                 # and free the request
 | |
|                 self.is_live = False
 | |
|                 self.close()
 | |
|                 return 0
 | |
|         # copy from int32array to python bytes
 | |
|         ret_length = min(self.read_len, len(memoryview(byte_obj)))
 | |
|         subarray = self.byte_buffer.subarray(
 | |
|             self.read_pos, self.read_pos + ret_length
 | |
|         ).to_py()
 | |
|         memoryview(byte_obj)[0:ret_length] = subarray
 | |
|         self.read_len -= ret_length
 | |
|         self.read_pos += ret_length
 | |
|         return ret_length
 | |
| 
 | |
| 
 | |
| class _StreamingFetcher:
 | |
|     def __init__(self) -> None:
 | |
|         # make web-worker and data buffer on startup
 | |
|         self.streaming_ready = False
 | |
| 
 | |
|         js_data_blob = js.Blob.new(
 | |
|             to_js([_STREAMING_WORKER_CODE], create_pyproxies=False),
 | |
|             _obj_from_dict({"type": "application/javascript"}),
 | |
|         )
 | |
| 
 | |
|         def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None:
 | |
|             def onMsg(e: JsProxy) -> None:
 | |
|                 self.streaming_ready = True
 | |
|                 js_resolve_fn(e)
 | |
| 
 | |
|             def onErr(e: JsProxy) -> None:
 | |
|                 js_reject_fn(e)  # Defensive: never happens in ci
 | |
| 
 | |
|             self.js_worker.onmessage = onMsg
 | |
|             self.js_worker.onerror = onErr
 | |
| 
 | |
|         js_data_url = js.URL.createObjectURL(js_data_blob)
 | |
|         self.js_worker = js.globalThis.Worker.new(js_data_url)
 | |
|         self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver)
 | |
| 
 | |
|     def send(self, request: EmscriptenRequest) -> EmscriptenResponse:
 | |
|         headers = {
 | |
|             k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE
 | |
|         }
 | |
| 
 | |
|         body = request.body
 | |
|         fetch_data = {"headers": headers, "body": to_js(body), "method": request.method}
 | |
|         # start the request off in the worker
 | |
|         timeout = int(1000 * request.timeout) if request.timeout > 0 else None
 | |
|         js_shared_buffer = js.SharedArrayBuffer.new(1048576)
 | |
|         js_int_buffer = js.Int32Array.new(js_shared_buffer)
 | |
|         js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8)
 | |
| 
 | |
|         js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT)
 | |
|         js.Atomics.notify(js_int_buffer, 0)
 | |
|         js_absolute_url = js.URL.new(request.url, js.location).href
 | |
|         self.js_worker.postMessage(
 | |
|             _obj_from_dict(
 | |
|                 {
 | |
|                     "buffer": js_shared_buffer,
 | |
|                     "url": js_absolute_url,
 | |
|                     "fetchParams": fetch_data,
 | |
|                 }
 | |
|             )
 | |
|         )
 | |
|         # wait for the worker to send something
 | |
|         js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout)
 | |
|         if js_int_buffer[0] == ERROR_TIMEOUT:
 | |
|             raise _TimeoutError(
 | |
|                 "Timeout connecting to streaming request",
 | |
|                 request=request,
 | |
|                 response=None,
 | |
|             )
 | |
|         elif js_int_buffer[0] == SUCCESS_HEADER:
 | |
|             # got response
 | |
|             # header length is in second int of intBuffer
 | |
|             string_len = js_int_buffer[1]
 | |
|             # decode the rest to a JSON string
 | |
|             js_decoder = js.TextDecoder.new()
 | |
|             # this does a copy (the slice) because decode can't work on shared array
 | |
|             # for some silly reason
 | |
|             json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len))
 | |
|             # get it as an object
 | |
|             response_obj = json.loads(json_str)
 | |
|             return EmscriptenResponse(
 | |
|                 request=request,
 | |
|                 status_code=response_obj["status"],
 | |
|                 headers=response_obj["headers"],
 | |
|                 body=_ReadStream(
 | |
|                     js_int_buffer,
 | |
|                     js_byte_buffer,
 | |
|                     request.timeout,
 | |
|                     self.js_worker,
 | |
|                     response_obj["connectionID"],
 | |
|                     request,
 | |
|                 ),
 | |
|             )
 | |
|         elif js_int_buffer[0] == ERROR_EXCEPTION:
 | |
|             string_len = js_int_buffer[1]
 | |
|             # decode the error string
 | |
|             js_decoder = js.TextDecoder.new()
 | |
|             json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len))
 | |
|             raise _StreamingError(
 | |
|                 f"Exception thrown in fetch: {json_str}", request=request, response=None
 | |
|             )
 | |
|         else:
 | |
|             raise _StreamingError(
 | |
|                 f"Unknown status from worker in fetch: {js_int_buffer[0]}",
 | |
|                 request=request,
 | |
|                 response=None,
 | |
|             )
 | |
| 
 | |
| 
 | |
| class _JSPIReadStream(io.RawIOBase):
 | |
|     """
 | |
|     A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch
 | |
|     response. This requires support for WebAssembly JavaScript Promise Integration
 | |
|     in the containing browser, and for pyodide to be launched via runPythonAsync.
 | |
| 
 | |
|     :param js_read_stream:
 | |
|         The JavaScript stream reader
 | |
| 
 | |
|     :param timeout:
 | |
|         Timeout in seconds
 | |
| 
 | |
|     :param request:
 | |
|         The request we're handling
 | |
| 
 | |
|     :param response:
 | |
|         The response this stream relates to
 | |
| 
 | |
|     :param js_abort_controller:
 | |
|         A JavaScript AbortController object, used for timeouts
 | |
|     """
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         js_read_stream: Any,
 | |
|         timeout: float,
 | |
|         request: EmscriptenRequest,
 | |
|         response: EmscriptenResponse,
 | |
|         js_abort_controller: Any,  # JavaScript AbortController for timeouts
 | |
|     ):
 | |
|         self.js_read_stream = js_read_stream
 | |
|         self.timeout = timeout
 | |
|         self._is_closed = False
 | |
|         self._is_done = False
 | |
|         self.request: EmscriptenRequest | None = request
 | |
|         self.response: EmscriptenResponse | None = response
 | |
|         self.current_buffer = None
 | |
|         self.current_buffer_pos = 0
 | |
|         self.js_abort_controller = js_abort_controller
 | |
| 
 | |
|     def __del__(self) -> None:
 | |
|         self.close()
 | |
| 
 | |
|     # this is compatible with _base_connection
 | |
|     def is_closed(self) -> bool:
 | |
|         return self._is_closed
 | |
| 
 | |
|     # for compatibility with RawIOBase
 | |
|     @property
 | |
|     def closed(self) -> bool:
 | |
|         return self.is_closed()
 | |
| 
 | |
|     def close(self) -> None:
 | |
|         if self.is_closed():
 | |
|             return
 | |
|         self.read_len = 0
 | |
|         self.read_pos = 0
 | |
|         self.js_read_stream.cancel()
 | |
|         self.js_read_stream = None
 | |
|         self._is_closed = True
 | |
|         self._is_done = True
 | |
|         self.request = None
 | |
|         self.response = None
 | |
|         super().close()
 | |
| 
 | |
|     def readable(self) -> bool:
 | |
|         return True
 | |
| 
 | |
|     def writable(self) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def seekable(self) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def _get_next_buffer(self) -> bool:
 | |
|         result_js = _run_sync_with_timeout(
 | |
|             self.js_read_stream.read(),
 | |
|             self.timeout,
 | |
|             self.js_abort_controller,
 | |
|             request=self.request,
 | |
|             response=self.response,
 | |
|         )
 | |
|         if result_js.done:
 | |
|             self._is_done = True
 | |
|             return False
 | |
|         else:
 | |
|             self.current_buffer = result_js.value.to_py()
 | |
|             self.current_buffer_pos = 0
 | |
|             return True
 | |
| 
 | |
|     def readinto(self, byte_obj: Buffer) -> int:
 | |
|         if self.current_buffer is None:
 | |
|             if not self._get_next_buffer() or self.current_buffer is None:
 | |
|                 self.close()
 | |
|                 return 0
 | |
|         ret_length = min(
 | |
|             len(byte_obj), len(self.current_buffer) - self.current_buffer_pos
 | |
|         )
 | |
|         byte_obj[0:ret_length] = self.current_buffer[
 | |
|             self.current_buffer_pos : self.current_buffer_pos + ret_length
 | |
|         ]
 | |
|         self.current_buffer_pos += ret_length
 | |
|         if self.current_buffer_pos == len(self.current_buffer):
 | |
|             self.current_buffer = None
 | |
|         return ret_length
 | |
| 
 | |
| 
 | |
| # check if we are in a worker or not
 | |
| def is_in_browser_main_thread() -> bool:
 | |
|     return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window
 | |
| 
 | |
| 
 | |
| def is_cross_origin_isolated() -> bool:
 | |
|     return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated
 | |
| 
 | |
| 
 | |
| def is_in_node() -> bool:
 | |
|     return (
 | |
|         hasattr(js, "process")
 | |
|         and hasattr(js.process, "release")
 | |
|         and hasattr(js.process.release, "name")
 | |
|         and js.process.release.name == "node"
 | |
|     )
 | |
| 
 | |
| 
 | |
| def is_worker_available() -> bool:
 | |
|     return hasattr(js, "Worker") and hasattr(js, "Blob")
 | |
| 
 | |
| 
 | |
| _fetcher: _StreamingFetcher | None = None
 | |
| 
 | |
| if is_worker_available() and (
 | |
|     (is_cross_origin_isolated() and not is_in_browser_main_thread())
 | |
|     and (not is_in_node())
 | |
| ):
 | |
|     _fetcher = _StreamingFetcher()
 | |
| else:
 | |
|     _fetcher = None
 | |
| 
 | |
| 
 | |
| NODE_JSPI_ERROR = (
 | |
|     "urllib3 only works in Node.js with pyodide.runPythonAsync"
 | |
|     " and requires the flag --experimental-wasm-stack-switching in "
 | |
|     " versions of node <24."
 | |
| )
 | |
| 
 | |
| 
 | |
| def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None:
 | |
|     if has_jspi():
 | |
|         return send_jspi_request(request, True)
 | |
|     elif is_in_node():
 | |
|         raise _RequestError(
 | |
|             message=NODE_JSPI_ERROR,
 | |
|             request=request,
 | |
|             response=None,
 | |
|         )
 | |
| 
 | |
|     if _fetcher and streaming_ready():
 | |
|         return _fetcher.send(request)
 | |
|     else:
 | |
|         _show_streaming_warning()
 | |
|         return None
 | |
| 
 | |
| 
 | |
| _SHOWN_TIMEOUT_WARNING = False
 | |
| 
 | |
| 
 | |
| def _show_timeout_warning() -> None:
 | |
|     global _SHOWN_TIMEOUT_WARNING
 | |
|     if not _SHOWN_TIMEOUT_WARNING:
 | |
|         _SHOWN_TIMEOUT_WARNING = True
 | |
|         message = "Warning: Timeout is not available on main browser thread"
 | |
|         js.console.warn(message)
 | |
| 
 | |
| 
 | |
| _SHOWN_STREAMING_WARNING = False
 | |
| 
 | |
| 
 | |
| def _show_streaming_warning() -> None:
 | |
|     global _SHOWN_STREAMING_WARNING
 | |
|     if not _SHOWN_STREAMING_WARNING:
 | |
|         _SHOWN_STREAMING_WARNING = True
 | |
|         message = "Can't stream HTTP requests because: \n"
 | |
|         if not is_cross_origin_isolated():
 | |
|             message += "  Page is not cross-origin isolated\n"
 | |
|         if is_in_browser_main_thread():
 | |
|             message += "  Python is running in main browser thread\n"
 | |
|         if not is_worker_available():
 | |
|             message += " Worker or Blob classes are not available in this environment."  # Defensive: this is always False in browsers that we test in
 | |
|         if streaming_ready() is False:
 | |
|             message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch
 | |
| is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`"""
 | |
|         from js import console
 | |
| 
 | |
|         console.warn(message)
 | |
| 
 | |
| 
 | |
| def send_request(request: EmscriptenRequest) -> EmscriptenResponse:
 | |
|     if has_jspi():
 | |
|         return send_jspi_request(request, False)
 | |
|     elif is_in_node():
 | |
|         raise _RequestError(
 | |
|             message=NODE_JSPI_ERROR,
 | |
|             request=request,
 | |
|             response=None,
 | |
|         )
 | |
|     try:
 | |
|         js_xhr = js.XMLHttpRequest.new()
 | |
| 
 | |
|         if not is_in_browser_main_thread():
 | |
|             js_xhr.responseType = "arraybuffer"
 | |
|             if request.timeout:
 | |
|                 js_xhr.timeout = int(request.timeout * 1000)
 | |
|         else:
 | |
|             js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15")
 | |
|             if request.timeout:
 | |
|                 # timeout isn't available on the main thread - show a warning in console
 | |
|                 # if it is set
 | |
|                 _show_timeout_warning()
 | |
| 
 | |
|         js_xhr.open(request.method, request.url, False)
 | |
|         for name, value in request.headers.items():
 | |
|             if name.lower() not in HEADERS_TO_IGNORE:
 | |
|                 js_xhr.setRequestHeader(name, value)
 | |
| 
 | |
|         js_xhr.send(to_js(request.body))
 | |
| 
 | |
|         headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders()))
 | |
| 
 | |
|         if not is_in_browser_main_thread():
 | |
|             body = js_xhr.response.to_py().tobytes()
 | |
|         else:
 | |
|             body = js_xhr.response.encode("ISO-8859-15")
 | |
|         return EmscriptenResponse(
 | |
|             status_code=js_xhr.status, headers=headers, body=body, request=request
 | |
|         )
 | |
|     except JsException as err:
 | |
|         if err.name == "TimeoutError":
 | |
|             raise _TimeoutError(err.message, request=request)
 | |
|         elif err.name == "NetworkError":
 | |
|             raise _RequestError(err.message, request=request)
 | |
|         else:
 | |
|             # general http error
 | |
|             raise _RequestError(err.message, request=request)
 | |
| 
 | |
| 
 | |
| def send_jspi_request(
 | |
|     request: EmscriptenRequest, streaming: bool
 | |
| ) -> EmscriptenResponse:
 | |
|     """
 | |
|     Send a request using WebAssembly JavaScript Promise Integration
 | |
|     to wrap the asynchronous JavaScript fetch api (experimental).
 | |
| 
 | |
|     :param request:
 | |
|         Request to send
 | |
| 
 | |
|     :param streaming:
 | |
|         Whether to stream the response
 | |
| 
 | |
|     :return: The response object
 | |
|     :rtype: EmscriptenResponse
 | |
|     """
 | |
|     timeout = request.timeout
 | |
|     js_abort_controller = js.AbortController.new()
 | |
|     headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE}
 | |
|     req_body = request.body
 | |
|     fetch_data = {
 | |
|         "headers": headers,
 | |
|         "body": to_js(req_body),
 | |
|         "method": request.method,
 | |
|         "signal": js_abort_controller.signal,
 | |
|     }
 | |
|     # Call JavaScript fetch (async api, returns a promise)
 | |
|     fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data))
 | |
|     # Now suspend WebAssembly until we resolve that promise
 | |
|     # or time out.
 | |
|     response_js = _run_sync_with_timeout(
 | |
|         fetcher_promise_js,
 | |
|         timeout,
 | |
|         js_abort_controller,
 | |
|         request=request,
 | |
|         response=None,
 | |
|     )
 | |
|     headers = {}
 | |
|     header_iter = response_js.headers.entries()
 | |
|     while True:
 | |
|         iter_value_js = header_iter.next()
 | |
|         if getattr(iter_value_js, "done", False):
 | |
|             break
 | |
|         else:
 | |
|             headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1])
 | |
|     status_code = response_js.status
 | |
|     body: bytes | io.RawIOBase = b""
 | |
| 
 | |
|     response = EmscriptenResponse(
 | |
|         status_code=status_code, headers=headers, body=b"", request=request
 | |
|     )
 | |
|     if streaming:
 | |
|         # get via inputstream
 | |
|         if response_js.body is not None:
 | |
|             # get a reader from the fetch response
 | |
|             body_stream_js = response_js.body.getReader()
 | |
|             body = _JSPIReadStream(
 | |
|                 body_stream_js, timeout, request, response, js_abort_controller
 | |
|             )
 | |
|     else:
 | |
|         # get directly via arraybuffer
 | |
|         # n.b. this is another async JavaScript call.
 | |
|         body = _run_sync_with_timeout(
 | |
|             response_js.arrayBuffer(),
 | |
|             timeout,
 | |
|             js_abort_controller,
 | |
|             request=request,
 | |
|             response=response,
 | |
|         ).to_py()
 | |
|     response.body = body
 | |
|     return response
 | |
| 
 | |
| 
 | |
| def _run_sync_with_timeout(
 | |
|     promise: Any,
 | |
|     timeout: float,
 | |
|     js_abort_controller: Any,
 | |
|     request: EmscriptenRequest | None,
 | |
|     response: EmscriptenResponse | None,
 | |
| ) -> Any:
 | |
|     """
 | |
|     Await a JavaScript promise synchronously with a timeout which is implemented
 | |
|     via the AbortController
 | |
| 
 | |
|     :param promise:
 | |
|         Javascript promise to await
 | |
| 
 | |
|     :param timeout:
 | |
|         Timeout in seconds
 | |
| 
 | |
|     :param js_abort_controller:
 | |
|         A JavaScript AbortController object, used on timeout
 | |
| 
 | |
|     :param request:
 | |
|         The request being handled
 | |
| 
 | |
|     :param response:
 | |
|         The response being handled (if it exists yet)
 | |
| 
 | |
|     :raises _TimeoutError: If the request times out
 | |
|     :raises _RequestError: If the request raises a JavaScript exception
 | |
| 
 | |
|     :return: The result of awaiting the promise.
 | |
|     """
 | |
|     timer_id = None
 | |
|     if timeout > 0:
 | |
|         timer_id = js.setTimeout(
 | |
|             js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000)
 | |
|         )
 | |
|     try:
 | |
|         from pyodide.ffi import run_sync
 | |
| 
 | |
|         # run_sync here uses WebAssembly JavaScript Promise Integration to
 | |
|         # suspend python until the JavaScript promise resolves.
 | |
|         return run_sync(promise)
 | |
|     except JsException as err:
 | |
|         if err.name == "AbortError":
 | |
|             raise _TimeoutError(
 | |
|                 message="Request timed out", request=request, response=response
 | |
|             )
 | |
|         else:
 | |
|             raise _RequestError(message=err.message, request=request, response=response)
 | |
|     finally:
 | |
|         if timer_id is not None:
 | |
|             js.clearTimeout(timer_id)
 | |
| 
 | |
| 
 | |
| def has_jspi() -> bool:
 | |
|     """
 | |
|     Return true if jspi can be used.
 | |
| 
 | |
|     This requires both browser support and also WebAssembly
 | |
|     to be in the correct state - i.e. that the javascript
 | |
|     call into python was async not sync.
 | |
| 
 | |
|     :return: True if jspi can be used.
 | |
|     :rtype: bool
 | |
|     """
 | |
|     try:
 | |
|         from pyodide.ffi import can_run_sync, run_sync  # noqa: F401
 | |
| 
 | |
|         return bool(can_run_sync())
 | |
|     except ImportError:
 | |
|         return False
 | |
| 
 | |
| 
 | |
| def streaming_ready() -> bool | None:
 | |
|     if _fetcher:
 | |
|         return _fetcher.streaming_ready
 | |
|     else:
 | |
|         return None  # no fetcher, return None to signify that
 | |
| 
 | |
| 
 | |
| async def wait_for_streaming_ready() -> bool:
 | |
|     if _fetcher:
 | |
|         await _fetcher.js_worker_ready_promise
 | |
|         return True
 | |
|     else:
 | |
|         return False
 | 
