pyodide-pool 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/.gitignore +1 -1
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/PKG-INFO +3 -3
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/README.md +2 -2
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/pyodide_pool/__init__.py +1 -1
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/pyodide_pool/_bridge.py +4 -1
- pyodide_pool-0.2.0/pyodide_pool/loader.py +70 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/pyproject.toml +4 -0
- pyodide_pool-0.1.2/pyodide_pool/loader.py +0 -99
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/pyodide_pool/_packages.py +0 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/pyodide_pool/scheduler.py +0 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/wasm_multiprocessing/README.md +0 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/wasm_multiprocessing/pyproject.toml +0 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/wasm_multiprocessing/wasm_multiprocessing/__init__.py +0 -0
- {pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/wasm_multiprocessing/wasm_multiprocessing/pool.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyodide-pool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Run Python callables and dask graphs on a pool of Pyodide Web Workers (driver-side API)
|
|
5
5
|
Project-URL: Homepage, https://github.com/fideus-labs/python-wasm-parallelism
|
|
6
6
|
Project-URL: Demo, https://fideus-labs.github.io/python-wasm-parallelism/
|
|
@@ -60,8 +60,8 @@ value = await pyodide_pool.submit(fn, *args, **kwargs) # one remote call
|
|
|
60
60
|
total = await pyodide_pool.compute(dask.delayed(sum)([...])) # dask collections
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`create_pool`
|
|
64
|
-
|
|
63
|
+
`create_pool` imports the self-contained JS bundle that ships inside the
|
|
64
|
+
wheel; pass `js_url=` to load a hosted copy instead. A web app that embeds
|
|
65
65
|
Pyodide can instead create the JS `PyodidePool` itself and register it with
|
|
66
66
|
`pyodide.registerJsModule('js_pyodide_pool', { pool })`.
|
|
67
67
|
|
|
@@ -36,8 +36,8 @@ value = await pyodide_pool.submit(fn, *args, **kwargs) # one remote call
|
|
|
36
36
|
total = await pyodide_pool.compute(dask.delayed(sum)([...])) # dask collections
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`create_pool`
|
|
40
|
-
|
|
39
|
+
`create_pool` imports the self-contained JS bundle that ships inside the
|
|
40
|
+
wheel; pass `js_url=` to load a hosted copy instead. A web app that embeds
|
|
41
41
|
Pyodide can instead create the JS `PyodidePool` itself and register it with
|
|
42
42
|
`pyodide.registerJsModule('js_pyodide_pool', { pool })`.
|
|
43
43
|
|
|
@@ -171,7 +171,10 @@ class WorkerPool:
|
|
|
171
171
|
|
|
172
172
|
The call triple ships as a cloudpickle payload together with the
|
|
173
173
|
driver's current package snapshot, so the worker mirrors the
|
|
174
|
-
driver's installed packages before executing.
|
|
174
|
+
driver's installed packages before executing. Where the runtime has
|
|
175
|
+
JSPI, the worker runs the call with stack switching: ``func`` may
|
|
176
|
+
block on an awaitable with ``pyodide.ffi.run_sync`` and may be a
|
|
177
|
+
coroutine function, whose awaited value is the result.
|
|
175
178
|
"""
|
|
176
179
|
payload = _to_js_buffer(cloudpickle.dumps((func, args, kwargs)))
|
|
177
180
|
packages, wheels = snapshot_packages()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Bootstrap the JS worker pool from inside a Pyodide kernel (JupyterLite).
|
|
2
|
+
|
|
3
|
+
Runs in the MAIN Pyodide instance (the driver), like the rest of the
|
|
4
|
+
package. An embedding web app wires the JS side itself (create a
|
|
5
|
+
``PyodidePool``, ``pyodide.registerJsModule('js_pyodide_pool', { pool })``),
|
|
6
|
+
but notebook code inside JupyterLite's Pyodide kernel has no JS embedder to
|
|
7
|
+
lean on — the kernel itself runs in a Web Worker. :func:`create_pool` pulls
|
|
8
|
+
the JS side in from Python instead: dynamically ``import()`` the
|
|
9
|
+
self-contained browser bundle (``pyodide-pool.browser.js``, built by
|
|
10
|
+
``npm run build`` and shipped inside this wheel), construct the JS
|
|
11
|
+
``PyodidePool`` — its workers spawn as NESTED Web Workers from a Blob URL
|
|
12
|
+
inlined in the bundle — and install the Python :class:`WorkerPool` wrapper
|
|
13
|
+
as the default pool, so ``pyodide_pool.submit``/``compute`` work with no
|
|
14
|
+
further setup.
|
|
15
|
+
|
|
16
|
+
The vendored bundle is imported from a ``data:`` URL, so no host has to
|
|
17
|
+
serve it; a page with a ``script-src`` CSP must allow ``data:`` (the nested
|
|
18
|
+
workers already need ``blob:``).
|
|
19
|
+
|
|
20
|
+
Environment requirements: nested-worker support (all Chromium-based
|
|
21
|
+
browsers; Firefox and Safari in current versions) and network access to
|
|
22
|
+
the Pyodide CDN. The pool is postMessage-based, so it does NOT need a
|
|
23
|
+
cross-origin-isolated page — it runs on plain static hosts like GitHub
|
|
24
|
+
Pages (verified by the Pages deployment of demos/jupyterlite). COOP/COEP
|
|
25
|
+
headers (scripts/serve-lite.mjs sends them) only add SharedArrayBuffer
|
|
26
|
+
extras such as the JupyterLite kernel's coincident transport.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from base64 import b64encode
|
|
32
|
+
from importlib.resources import files
|
|
33
|
+
|
|
34
|
+
from . import _bridge
|
|
35
|
+
from ._bridge import WorkerPool
|
|
36
|
+
|
|
37
|
+
__all__ = ["create_pool"]
|
|
38
|
+
|
|
39
|
+
#: The browser bundle, copied next to this module by scripts/bundles.mjs and
|
|
40
|
+
#: listed as a build artifact in pyproject.toml so the wheel carries it.
|
|
41
|
+
_BUNDLE = "pyodide-pool.browser.js"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def create_pool(pool_size: int = 4, js_url: str | None = None) -> WorkerPool:
|
|
45
|
+
"""Import the JS bundle, start a worker pool, and make it the default.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
pool_size:
|
|
50
|
+
Maximum number of concurrent workers, one Pyodide interpreter each.
|
|
51
|
+
js_url:
|
|
52
|
+
Load the bundle from this URL instead of the vendored copy. Must be
|
|
53
|
+
fully qualified (``https://``, or ``file://`` under Node): inside a
|
|
54
|
+
kernel worker, ``import()`` resolves a relative URL against the CDN
|
|
55
|
+
module evaluating it, not against the site.
|
|
56
|
+
"""
|
|
57
|
+
from js import Object
|
|
58
|
+
from pyodide.code import run_js
|
|
59
|
+
from pyodide.ffi import to_js
|
|
60
|
+
|
|
61
|
+
if js_url is None:
|
|
62
|
+
# FileNotFoundError here means the wheel was built before the JS
|
|
63
|
+
# bundle (run `npm run build` first).
|
|
64
|
+
source = files(__package__).joinpath(_BUNDLE).read_bytes()
|
|
65
|
+
js_url = "data:text/javascript;base64," + b64encode(source).decode()
|
|
66
|
+
module = await run_js("(url) => import(url)")(js_url)
|
|
67
|
+
options = to_js({"poolSize": pool_size}, dict_converter=Object.fromEntries)
|
|
68
|
+
pool = WorkerPool(module.createPool(options))
|
|
69
|
+
_bridge.set_default_pool(pool)
|
|
70
|
+
return pool
|
|
@@ -40,3 +40,7 @@ path = "pyodide_pool/__init__.py"
|
|
|
40
40
|
|
|
41
41
|
[tool.hatch.build.targets.wheel]
|
|
42
42
|
packages = ["pyodide_pool"]
|
|
43
|
+
# Build output written by scripts/bundles.mjs and gitignored at the repo root.
|
|
44
|
+
# Hatchling only reads a .gitignore inside this directory today, so the file
|
|
45
|
+
# is included either way; naming it keeps the wheel whole if that changes.
|
|
46
|
+
artifacts = ["pyodide_pool/pyodide-pool.browser.js"]
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Bootstrap the JS worker pool from inside a Pyodide kernel (JupyterLite).
|
|
2
|
-
|
|
3
|
-
Runs in the MAIN Pyodide instance (the driver), like the rest of the
|
|
4
|
-
package. An embedding web app wires the JS side itself (create a
|
|
5
|
-
``PyodidePool``, ``pyodide.registerJsModule('js_pyodide_pool', { pool })``),
|
|
6
|
-
but notebook code inside JupyterLite's Pyodide kernel has no JS embedder to
|
|
7
|
-
lean on — the kernel itself runs in a Web Worker. :func:`create_pool` pulls
|
|
8
|
-
the JS side in from Python instead: dynamically ``import()`` the
|
|
9
|
-
self-contained browser bundle (``pyodide-pool.browser.js``, built by
|
|
10
|
-
``npm run build`` and copied into the site's ``files/assets/`` by
|
|
11
|
-
``npm run build:lite``), construct the JS ``PyodidePool`` — its workers
|
|
12
|
-
spawn as NESTED Web Workers from a Blob URL inlined in the bundle — and
|
|
13
|
-
install the Python :class:`WorkerPool` wrapper as the default pool, so
|
|
14
|
-
``pyodide_pool.submit``/``compute`` work with no further setup.
|
|
15
|
-
|
|
16
|
-
Environment requirements: nested-worker support (all Chromium-based
|
|
17
|
-
browsers; Firefox and Safari in current versions) and network access to
|
|
18
|
-
the Pyodide CDN. The pool is postMessage-based, so it does NOT need a
|
|
19
|
-
cross-origin-isolated page — it runs on plain static hosts like GitHub
|
|
20
|
-
Pages (verified by the Pages deployment of demos/jupyterlite). COOP/COEP
|
|
21
|
-
headers (scripts/serve-lite.mjs sends them) only add SharedArrayBuffer
|
|
22
|
-
extras such as the JupyterLite kernel's coincident transport.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from __future__ import annotations
|
|
26
|
-
|
|
27
|
-
from . import _bridge
|
|
28
|
-
from ._bridge import WorkerPool
|
|
29
|
-
|
|
30
|
-
__all__ = ["DEFAULT_JS_URL", "create_pool"]
|
|
31
|
-
|
|
32
|
-
#: Where the JupyterLite site serves the bundle, as a path RELATIVE to the
|
|
33
|
-
#: deployment's base URL (``files/`` holds the site contents). It is resolved
|
|
34
|
-
#: against the base URL derived from the kernel worker's ``location`` (see
|
|
35
|
-
#: :data:`_IMPORT_JS`), so the default works whether the site is hosted at the
|
|
36
|
-
#: origin root (scripts/serve-lite.mjs) or under a sub-path such as GitHub
|
|
37
|
-
#: Pages (``/<repo>/``) — no explicit ``js_url`` needed either way.
|
|
38
|
-
DEFAULT_JS_URL = "files/assets/pyodide-pool.browser.js"
|
|
39
|
-
|
|
40
|
-
# JS that dynamically ``import()``s the browser bundle. Two wrinkles make a
|
|
41
|
-
# plain ``import(url)`` wrong inside JupyterLite's Pyodide kernel:
|
|
42
|
-
# * ``import()`` resolves relative/path-absolute URLs against the REFERRER
|
|
43
|
-
# module — here pyodide.asm.js, loaded from the jsDelivr CDN — so a
|
|
44
|
-
# site-relative path would 404 on the CDN rather than the site.
|
|
45
|
-
# * A path-absolute ``/files/...`` resolved against the page origin drops
|
|
46
|
-
# any deployment sub-path, so GitHub Pages (``/<repo>/``) 404s on
|
|
47
|
-
# ``origin/files/...`` — the original symptom this bootstrap fixes.
|
|
48
|
-
# The kernel worker is served from ``<baseUrl>extensions/.../<name>.worker.js``
|
|
49
|
-
# (jupyterlite-pyodide-kernel's ``initWorker``), so everything up to the slash
|
|
50
|
-
# before ``extensions/`` is the deployment base URL. Resolve the base-relative
|
|
51
|
-
# bundle URL against that, falling back to the origin root. A fully-qualified
|
|
52
|
-
# URL (http:, https:, file:, blob:, data:) and Node (no global ``location``)
|
|
53
|
-
# are imported as given.
|
|
54
|
-
_IMPORT_JS = r"""
|
|
55
|
-
(async (u) => {
|
|
56
|
-
if (typeof location === 'undefined') return import(u);
|
|
57
|
-
if (/^[a-z][a-z0-9+.\-]*:/i.test(u)) return import(u);
|
|
58
|
-
const here = location.href;
|
|
59
|
-
const rel = u.replace(/^\/+/, '');
|
|
60
|
-
const ext = here.indexOf('/extensions/');
|
|
61
|
-
const base = ext === -1 ? new URL('/', here).href : here.slice(0, ext + 1);
|
|
62
|
-
const candidates = [...new Set([new URL(rel, base).href, new URL('/' + rel, here).href])];
|
|
63
|
-
let lastErr;
|
|
64
|
-
for (const c of candidates) {
|
|
65
|
-
try { return await import(c); } catch (e) { lastErr = e; }
|
|
66
|
-
}
|
|
67
|
-
throw lastErr;
|
|
68
|
-
})
|
|
69
|
-
""".strip()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
async def create_pool(pool_size: int = 4, js_url: str | None = None) -> WorkerPool:
|
|
73
|
-
"""Import the JS bundle, start a worker pool, and make it the default.
|
|
74
|
-
|
|
75
|
-
Parameters
|
|
76
|
-
----------
|
|
77
|
-
pool_size:
|
|
78
|
-
Maximum number of concurrent workers, one Pyodide interpreter each.
|
|
79
|
-
js_url:
|
|
80
|
-
URL of the self-contained browser bundle, defaulting to
|
|
81
|
-
:data:`DEFAULT_JS_URL`. A base-relative path (``files/...``) is
|
|
82
|
-
resolved against the JupyterLite deployment base URL; a full URL
|
|
83
|
-
(``http(s)://``, or a ``file://`` URL under Node) is used as-is.
|
|
84
|
-
"""
|
|
85
|
-
from js import Object
|
|
86
|
-
from pyodide.code import run_js
|
|
87
|
-
from pyodide.ffi import to_js
|
|
88
|
-
|
|
89
|
-
url = DEFAULT_JS_URL if js_url is None else js_url
|
|
90
|
-
# Resolve and import the bundle against the deployment base URL; see
|
|
91
|
-
# _IMPORT_JS for why a plain import() / path-absolute URL is wrong here.
|
|
92
|
-
# repr(url) yields a JS-safe string literal (matching the old f-string's
|
|
93
|
-
# {url!r}); build the call by concatenation so the JS body's braces are
|
|
94
|
-
# not read as Python format fields.
|
|
95
|
-
module = await run_js(_IMPORT_JS + "(" + repr(url) + ")")
|
|
96
|
-
options = to_js({"poolSize": pool_size}, dict_converter=Object.fromEntries)
|
|
97
|
-
pool = WorkerPool(module.createPool(options))
|
|
98
|
-
_bridge.set_default_pool(pool)
|
|
99
|
-
return pool
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyodide_pool-0.1.2 → pyodide_pool-0.2.0}/wasm_multiprocessing/wasm_multiprocessing/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|