blu-react 0.0.1__py3-none-any.whl
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.
- blu/__init__.py +74 -0
- blu/_app/__init__.py +463 -0
- blu/_app/client_main.py +159 -0
- blu/_app/render.py +107 -0
- blu/_app/router.py +267 -0
- blu/_cli.py +25 -0
- blu/_exceptions.py +5 -0
- blu/_hooks.py +323 -0
- blu/_http.py +219 -0
- blu/_nodes.py +771 -0
- blu/_server_functions.py +423 -0
- blu/_settings.py +30 -0
- blu/_utils/__init__.py +47 -0
- blu/_utils/asgi.py +189 -0
- blu/_utils/asyncio.py +60 -0
- blu/_utils/client.py +3 -0
- blu/_utils/json.py +30 -0
- blu/_utils/typing.py +33 -0
- blu/html.py +91 -0
- blu/util.js +334 -0
- blu_react-0.0.1.dist-info/METADATA +63 -0
- blu_react-0.0.1.dist-info/RECORD +25 -0
- blu_react-0.0.1.dist-info/WHEEL +4 -0
- blu_react-0.0.1.dist-info/entry_points.txt +2 -0
- blu_react-0.0.1.dist-info/licenses/LICENSE +21 -0
blu/_utils/asyncio.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
from blu._exceptions import WrongEnvironmentError
|
|
6
|
+
from blu._utils.typing import Any, cast
|
|
7
|
+
from blu._utils.client import is_client
|
|
8
|
+
|
|
9
|
+
if not is_client:
|
|
10
|
+
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
|
|
11
|
+
else:
|
|
12
|
+
from blu._utils.typing import type_place_holder
|
|
13
|
+
|
|
14
|
+
Executor = type_place_holder
|
|
15
|
+
ProcessPoolExecutor = type_place_holder
|
|
16
|
+
ThreadPoolExecutor = type_place_holder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def awaitable[T](input: T | Awaitable[T]) -> T:
|
|
20
|
+
if isinstance(input, Awaitable):
|
|
21
|
+
return await cast(Awaitable[T], input)
|
|
22
|
+
else:
|
|
23
|
+
return input
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_sync[**P, R](fn: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, R]:
|
|
27
|
+
def ret(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
28
|
+
return asyncio.run(fn(*args, **kwargs))
|
|
29
|
+
|
|
30
|
+
return ret
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def io_bound[
|
|
34
|
+
**P, R
|
|
35
|
+
](fn: Callable[P, R],) -> Callable[P, Coroutine[Any, Any, R]]:
|
|
36
|
+
return _asyncify(fn, ThreadPoolExecutor) # type: ignore
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cpu_bound[
|
|
40
|
+
**P, R
|
|
41
|
+
](fn: Callable[P, R],) -> Callable[P, Coroutine[Any, Any, R]]:
|
|
42
|
+
return _asyncify(fn, ProcessPoolExecutor) # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _asyncify[
|
|
46
|
+
**P, R
|
|
47
|
+
](fn: Callable[P, R], executor: type[Executor]) -> Callable[ # type: ignore
|
|
48
|
+
P, Awaitable[R]
|
|
49
|
+
]:
|
|
50
|
+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
51
|
+
if is_client:
|
|
52
|
+
raise WrongEnvironmentError(
|
|
53
|
+
f"{fn} can only be called in a server environment."
|
|
54
|
+
)
|
|
55
|
+
loop = asyncio.get_running_loop()
|
|
56
|
+
bound = functools.partial(fn, *args, **kwargs)
|
|
57
|
+
with executor() as pool: # type: ignore
|
|
58
|
+
return await loop.run_in_executor(pool, bound)
|
|
59
|
+
|
|
60
|
+
return wrapped
|
blu/_utils/client.py
ADDED
blu/_utils/json.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Iterable, Mapping
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from blu._utils.asyncio import io_bound
|
|
5
|
+
from blu._utils.client import is_client
|
|
6
|
+
|
|
7
|
+
type JsonData = (
|
|
8
|
+
None | bool | int | float | str | Iterable["JsonData"] | Mapping[str, "JsonData"]
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if is_client:
|
|
13
|
+
|
|
14
|
+
from js import JSON # type: ignore
|
|
15
|
+
|
|
16
|
+
async def loads(json_string: str) -> JsonData: # type: ignore
|
|
17
|
+
return JSON.parse(json_string) # type: ignore
|
|
18
|
+
|
|
19
|
+
async def dumps(json_data: JsonData) -> str: # type: ignore
|
|
20
|
+
return JSON.stringify(json_data) # type: ignore
|
|
21
|
+
|
|
22
|
+
else:
|
|
23
|
+
|
|
24
|
+
@io_bound
|
|
25
|
+
def loads(json_string: str) -> JsonData:
|
|
26
|
+
return json.loads(json_string)
|
|
27
|
+
|
|
28
|
+
@io_bound
|
|
29
|
+
def dumps(json_data: JsonData) -> str:
|
|
30
|
+
return json.dumps(json_data)
|
blu/_utils/typing.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class TypePlaceHolder:
|
|
2
|
+
def __getitem__(self, index): # type: ignore
|
|
3
|
+
return None
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type_place_holder = TypePlaceHolder()
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from typing import * # type: ignore
|
|
10
|
+
except ImportError:
|
|
11
|
+
|
|
12
|
+
for type_name in [
|
|
13
|
+
"Any",
|
|
14
|
+
"AsyncGenerator",
|
|
15
|
+
"AsyncIterable",
|
|
16
|
+
"AsyncIterator",
|
|
17
|
+
"Callable",
|
|
18
|
+
"Corouotine",
|
|
19
|
+
"Generator",
|
|
20
|
+
"Iterable",
|
|
21
|
+
"Iterator",
|
|
22
|
+
"Literal",
|
|
23
|
+
"Mapping",
|
|
24
|
+
"NotRequired",
|
|
25
|
+
"Optional",
|
|
26
|
+
"Protocol",
|
|
27
|
+
"Sequence",
|
|
28
|
+
"TypedDict",
|
|
29
|
+
]:
|
|
30
|
+
globals()[type_name] = type_place_holder
|
|
31
|
+
|
|
32
|
+
def cast(_, x): # type: ignore
|
|
33
|
+
return x # type: ignore
|
blu/html.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
React HTML elements.
|
|
3
|
+
|
|
4
|
+
.. code-block:: python
|
|
5
|
+
|
|
6
|
+
from blu.html import div, p
|
|
7
|
+
|
|
8
|
+
div(className='my-div')[
|
|
9
|
+
p['Hello!'],
|
|
10
|
+
p['Hi.'],
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
.. code-block:: html
|
|
14
|
+
|
|
15
|
+
<div className='my-div'>
|
|
16
|
+
<p>Hello!</p>
|
|
17
|
+
<p>Hi.</p>
|
|
18
|
+
</div>
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from blu._nodes import HTMLElement as _HTMLElement
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __getattr__(import_name: str) -> _HTMLElement:
|
|
25
|
+
"""
|
|
26
|
+
Creates a :class:`blu.HTMLElement` with the given
|
|
27
|
+
HTML tag name.
|
|
28
|
+
|
|
29
|
+
.. code-block:: python
|
|
30
|
+
|
|
31
|
+
from blu.html import div, p
|
|
32
|
+
|
|
33
|
+
div(className='my-div')[
|
|
34
|
+
p['Hello!'],
|
|
35
|
+
p['Hi.'],
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
.. code-block:: html
|
|
39
|
+
|
|
40
|
+
<div className='my-div'>
|
|
41
|
+
<p>Hello!</p>
|
|
42
|
+
<p>Hi.</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Use a trailing underscore to import tag names that are reserved
|
|
47
|
+
words in Python (the trailing underscore won't show up in the
|
|
48
|
+
resulting html):
|
|
49
|
+
|
|
50
|
+
.. code-block:: python
|
|
51
|
+
|
|
52
|
+
from blu.html import del_
|
|
53
|
+
|
|
54
|
+
del_['Hello, World!']
|
|
55
|
+
|
|
56
|
+
.. code-block:: html
|
|
57
|
+
|
|
58
|
+
<del>Hello, World!</del>
|
|
59
|
+
|
|
60
|
+
Non-trailing underscores will be converted to dashes:
|
|
61
|
+
|
|
62
|
+
.. code-block:: python
|
|
63
|
+
|
|
64
|
+
from blu.html import my_custom_element
|
|
65
|
+
|
|
66
|
+
my_custom_element['Hello!']
|
|
67
|
+
|
|
68
|
+
.. code-block:: html
|
|
69
|
+
|
|
70
|
+
<my-custom-element>Hello!</my-custom-element>
|
|
71
|
+
|
|
72
|
+
For the rare case where your desired HTML tag name cannot be
|
|
73
|
+
imported from :mod:`blu.html`, use
|
|
74
|
+
:func:`blu.create_rare_html_element`.
|
|
75
|
+
|
|
76
|
+
:param import_name: An HTML tag name.
|
|
77
|
+
|
|
78
|
+
:return: A :class:`blu.HTMLElement` whose tag name is
|
|
79
|
+
``import_name`` with the last trailing underscore (if any)
|
|
80
|
+
removed and any other underscores converted to dashes. The
|
|
81
|
+
:class:`HTMLElement <blu.HTMLElement>` won't have any props or
|
|
82
|
+
children.
|
|
83
|
+
"""
|
|
84
|
+
from blu._nodes import py_to_html_name
|
|
85
|
+
|
|
86
|
+
if import_name == "__mro__":
|
|
87
|
+
raise AttributeError(
|
|
88
|
+
"module 'blu.html' has no attribute '__mro__'",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return _HTMLElement(py_to_html_name(import_name), {}, [])
|
blu/util.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React from 'https://esm.sh/react';
|
|
2
|
+
import { createElement as $, Fragment } from 'https://esm.sh/react';
|
|
3
|
+
import { createRoot } from 'https://esm.sh/react-dom/client';
|
|
4
|
+
import * as PyScript from 'https://pyscript.net/releases/2026.1.1/core.js';
|
|
5
|
+
|
|
6
|
+
let create_proxy;
|
|
7
|
+
let blu;
|
|
8
|
+
let builtins;
|
|
9
|
+
let abc;
|
|
10
|
+
let sys;
|
|
11
|
+
let renderClientElement;
|
|
12
|
+
let useEffectInvoke;
|
|
13
|
+
let operator;
|
|
14
|
+
let useEffectCleanupAsyncGen;
|
|
15
|
+
|
|
16
|
+
export function init(innerPyImport) {
|
|
17
|
+
const ffi = innerPyImport('pyscript.ffi');
|
|
18
|
+
create_proxy = ffi.create_proxy(ffi.create_proxy);
|
|
19
|
+
function pyImport(location) {
|
|
20
|
+
return create_proxy(innerPyImport(location))
|
|
21
|
+
}
|
|
22
|
+
blu = pyImport('blu');
|
|
23
|
+
builtins = pyImport('builtins');
|
|
24
|
+
abc = pyImport('collections.abc');
|
|
25
|
+
sys = pyImport('sys');
|
|
26
|
+
operator = pyImport('operator');
|
|
27
|
+
renderClientElement = pyImport('blu._nodes')._render_client_element;
|
|
28
|
+
const hooksMod = pyImport('blu._hooks');
|
|
29
|
+
useEffectInvoke = getProxy(hooksMod._use_effect_invoke);
|
|
30
|
+
useEffectCleanupAsyncGen = getProxy(hooksMod._use_effect_cleanup_async_gen);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const idToProxyInfo = new Map();
|
|
34
|
+
const proxyToId = new Map();
|
|
35
|
+
|
|
36
|
+
const PRIMITIVE_TYPES = ['undefined', 'boolean', 'number', 'bigint', 'string', 'symbol'];
|
|
37
|
+
|
|
38
|
+
function getProxy(obj) {
|
|
39
|
+
if (obj === null || PRIMITIVE_TYPES.includes(typeof obj)) {
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
const id = builtins.id(obj);
|
|
43
|
+
if (!idToProxyInfo.has(id)) {
|
|
44
|
+
const proxy = create_proxy(obj)
|
|
45
|
+
idToProxyInfo.set(id, {
|
|
46
|
+
proxy,
|
|
47
|
+
count: 0
|
|
48
|
+
});
|
|
49
|
+
proxyToId.set(proxy, id);
|
|
50
|
+
}
|
|
51
|
+
const proxyInfo = idToProxyInfo.get(id);
|
|
52
|
+
proxyInfo.count++;
|
|
53
|
+
return proxyInfo.proxy;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function destroy(pyProxy) {
|
|
57
|
+
if (pyProxy === null || PRIMITIVE_TYPES.includes(typeof pyObj)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const id = proxyToId(pyProxy);
|
|
61
|
+
const proxyInfo = idToProxyInfo(id);
|
|
62
|
+
const { proxy, count } = proxyInfo;
|
|
63
|
+
if (count < 1) {
|
|
64
|
+
throw new Error(`Too few existing copies (${count}) when destroying ${proxy}`);
|
|
65
|
+
}
|
|
66
|
+
proxyInfo.count--;
|
|
67
|
+
if (proxyInfo.count === 0) {
|
|
68
|
+
proxyToId.delete(proxy);
|
|
69
|
+
idToProxyInfo.delete(id);
|
|
70
|
+
proxy.destroy();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function unwrap(thing) {
|
|
75
|
+
if ([undefined, null].includes(thing)) {
|
|
76
|
+
return thing;
|
|
77
|
+
}
|
|
78
|
+
while (thing.type === 'pyodide.ffi.JsProxy') {
|
|
79
|
+
thing = thing.unwrap();
|
|
80
|
+
}
|
|
81
|
+
return thing;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderRoot(pyRoot) {
|
|
85
|
+
const jsNode = getReactNode(pyRoot);
|
|
86
|
+
const renderTarget = jsNode.type === 'html' ? document : document.body;
|
|
87
|
+
createRoot(renderTarget).render(jsNode);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getReactNode(pyNode) {
|
|
91
|
+
if (isOfType(pyNode, blu.ClientElement)) {
|
|
92
|
+
return $(PythonElement, {
|
|
93
|
+
renderer: getProxy(pyNode._renderer),
|
|
94
|
+
args: getProxy(pyNode._args),
|
|
95
|
+
kwargs: getProxy(pyNode._kwargs),
|
|
96
|
+
pyChildren: getProxy(pyNode._children)
|
|
97
|
+
})
|
|
98
|
+
} else if (isOfType(pyNode, blu.Key)) {
|
|
99
|
+
return $(MemManagedFragment, { key: getProxy(pyNode._key) }, ...getArray(pyNode._children));
|
|
100
|
+
} else if (isOfType(pyNode, builtins.tuple)) {
|
|
101
|
+
return $(React.Fragment, null, ...getArray(pyNode));
|
|
102
|
+
} else if (typeof pyNode === 'string') {
|
|
103
|
+
return pyNode;
|
|
104
|
+
} else if (builtins.hasattr(pyNode, '__iter__')) {
|
|
105
|
+
return getArray(pyNode);
|
|
106
|
+
} else if (isOfType(pyNode, blu.HTMLElement)) {
|
|
107
|
+
const attrs = {};
|
|
108
|
+
let hasRef = false;
|
|
109
|
+
for (const [k, v] of pyNode._attrs.items()) {
|
|
110
|
+
if (k == 'ref') {
|
|
111
|
+
hasRef = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
attrs[k] = getProxy(v);
|
|
115
|
+
}
|
|
116
|
+
if (hasRef) {
|
|
117
|
+
attrs['ref'] = pyNode._attrs['ref']._ref_proxy;
|
|
118
|
+
}
|
|
119
|
+
return $(MemManagedHTMLElement, { _blu_tagname: pyNode._tagname, ...attrs },
|
|
120
|
+
...getArray(pyNode._children),
|
|
121
|
+
);
|
|
122
|
+
} else if (pyNode === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
} else {
|
|
125
|
+
return pyNode.toString();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getProxies(items) {
|
|
130
|
+
const ret = [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
ret.push(getProxy(item));
|
|
133
|
+
}
|
|
134
|
+
return ret
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function PythonElement(proxies) {
|
|
138
|
+
React.useEffect(() => () => {
|
|
139
|
+
destroy(proxies.renderer);
|
|
140
|
+
destroy(proxies.args);
|
|
141
|
+
destroy(proxies.kwargs);
|
|
142
|
+
destroy(proxies.pyChildren);
|
|
143
|
+
}, []);
|
|
144
|
+
const { renderer, args, kwargs, pyChildren } = unwrapProxyProps(proxies);
|
|
145
|
+
const result = renderer.callKwargs(...args, kwargs.toJs({ depth: 1 }));
|
|
146
|
+
let pyNode;
|
|
147
|
+
if (builtins.isinstance(result, abc.Generator)) {
|
|
148
|
+
result.next();
|
|
149
|
+
try {
|
|
150
|
+
if (pyChildren.length === 1) {
|
|
151
|
+
result.send(pyChildren[0]);
|
|
152
|
+
} else {
|
|
153
|
+
result.send(pyChildren);
|
|
154
|
+
}
|
|
155
|
+
} catch(e) {
|
|
156
|
+
if (e.name === 'PythonError') {
|
|
157
|
+
const pyExc = sys.last_exc;
|
|
158
|
+
if (isOfType(pyExc, builtins.StopIteration)) {
|
|
159
|
+
pyNode = pyExc.value;
|
|
160
|
+
} else {
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
pyNode = result;
|
|
169
|
+
}
|
|
170
|
+
return getReactNode(pyNode);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unwrapProxyProps(proxies) {
|
|
174
|
+
const ret = {}
|
|
175
|
+
for (const [k, v] of Object.entries(proxies)) {
|
|
176
|
+
ret[k] = unwrap(v);
|
|
177
|
+
}
|
|
178
|
+
return ret
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function MemManagedFragment({ key, children }) {
|
|
182
|
+
const childrenArray = getChildrenArray(children);
|
|
183
|
+
React.useEffect(() => () => {
|
|
184
|
+
destroy(key);
|
|
185
|
+
}, []);
|
|
186
|
+
return $(React.Fragment, { key }, ...childrenArray);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getChildrenArray(children) {
|
|
190
|
+
return children instanceof Array ? children : [children];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function unwrapChildren(children) {
|
|
194
|
+
if (children instanceof Array) {
|
|
195
|
+
return children.map(x => unwrap(x));
|
|
196
|
+
} else {
|
|
197
|
+
return unwrap(children);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function destroyChildren(children) {
|
|
202
|
+
if (children instanceof Array) {
|
|
203
|
+
for (const child of children) {
|
|
204
|
+
destroy(child);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
destroy(children);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function MemManagedHTMLElement({ _blu_tagname, children, ...attrs }) {
|
|
212
|
+
const childrenArray = getChildrenArray(children);
|
|
213
|
+
React.useEffect(() => () => {
|
|
214
|
+
for (const attr_value of Object.values(attrs)) {
|
|
215
|
+
destroy(attr_value);
|
|
216
|
+
}
|
|
217
|
+
}, []);
|
|
218
|
+
return $(_blu_tagname, attrs, ...childrenArray);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function childrenReactArray(children) {
|
|
222
|
+
const childrenArray = children instanceof Array ? children : [children];
|
|
223
|
+
return childrenArray.map(x => getReactNode(x));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getArray(pyIterable) {
|
|
227
|
+
pyIterable = unwrap(pyIterable);
|
|
228
|
+
const ret = [];
|
|
229
|
+
for (const item of pyIterable) {
|
|
230
|
+
ret.push(getReactNode(item));
|
|
231
|
+
}
|
|
232
|
+
return ret;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function useState(init) {
|
|
236
|
+
const initUsedRef = React.useRef(false);
|
|
237
|
+
const wrapped = getProxy(init);
|
|
238
|
+
const valueRef = React.useRef(wrapped);
|
|
239
|
+
React.useEffect(() => () => {
|
|
240
|
+
destroy(valueRef.current);
|
|
241
|
+
});
|
|
242
|
+
const [value, setValue] = React.useState(wrapped);
|
|
243
|
+
if (initUsedRef.current) {
|
|
244
|
+
destroy(wrapped);
|
|
245
|
+
}
|
|
246
|
+
initUsedRef.current = true;
|
|
247
|
+
function setter(newValue) {
|
|
248
|
+
destroy(valueRef.current);
|
|
249
|
+
const newProxy = getProxy(newValue);
|
|
250
|
+
valueRef.current = newProxy;
|
|
251
|
+
setValue(newProxy);
|
|
252
|
+
}
|
|
253
|
+
return [value, setter];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function useRefObj(pyRef) {
|
|
257
|
+
const refProxiedRef = React.useRef(false);
|
|
258
|
+
pyRef = getProxy(pyRef);
|
|
259
|
+
pyRef._ref_proxy = refProxy(pyRef);
|
|
260
|
+
const refRef = React.useRef(pyRef);
|
|
261
|
+
if (refProxiedRef.current) {
|
|
262
|
+
destroy(pyRef);
|
|
263
|
+
}
|
|
264
|
+
refProxiedRef.current = true;
|
|
265
|
+
React.useEffect(() => () => {
|
|
266
|
+
destroy(refRef.current);
|
|
267
|
+
}, []);
|
|
268
|
+
return refRef.current;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function refProxy(pyRef) {
|
|
272
|
+
return {
|
|
273
|
+
set current(newValue) {
|
|
274
|
+
pyRef._value = newValue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function useEffect(callback) {
|
|
280
|
+
const callbackProxy = getProxy(callback);
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
const packed = useEffectInvoke(callbackProxy);
|
|
283
|
+
const tag = operator.getitem(packed, 0).toString();
|
|
284
|
+
const result = operator.getitem(packed, 1);
|
|
285
|
+
if (tag === 'async_generator') {
|
|
286
|
+
const agen = getProxy(result);
|
|
287
|
+
return () => {
|
|
288
|
+
const onDone = create_proxy(() => {
|
|
289
|
+
destroy(agen);
|
|
290
|
+
onDone.destroy();
|
|
291
|
+
});
|
|
292
|
+
useEffectCleanupAsyncGen(agen, onDone);
|
|
293
|
+
destroy(callbackProxy);
|
|
294
|
+
};
|
|
295
|
+
} else if (tag === 'coroutine') {
|
|
296
|
+
const coro = getProxy(result);
|
|
297
|
+
return () => {
|
|
298
|
+
try {
|
|
299
|
+
coro.close();
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// ignore close errors during unmount
|
|
302
|
+
}
|
|
303
|
+
destroy(coro);
|
|
304
|
+
destroy(callbackProxy);
|
|
305
|
+
};
|
|
306
|
+
} else if (tag === 'generator') {
|
|
307
|
+
const generator = getProxy(result);
|
|
308
|
+
generator.next();
|
|
309
|
+
return () => {
|
|
310
|
+
generator.next();
|
|
311
|
+
destroy(generator);
|
|
312
|
+
destroy(callbackProxy);
|
|
313
|
+
};
|
|
314
|
+
} else {
|
|
315
|
+
return () => {
|
|
316
|
+
destroy(callbackProxy);
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isOfType(obj, pyClass) {
|
|
323
|
+
if (pyClass === undefined || !pyClass.toString().match(/^<class '[_A-Za-z0-9\.]+'>$/g)) {
|
|
324
|
+
throw Error('Second argument must be a Python class.')
|
|
325
|
+
}
|
|
326
|
+
if (obj?.__class__ === undefined) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return obj.__class__.toString() === pyClass.toString();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function pyIs(obj1, obj2) {
|
|
333
|
+
return obj1.__repr__() === obj2.__repr__()
|
|
334
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blu-react
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A full stack React framework for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/johnlarson/blu
|
|
6
|
+
Project-URL: Issues, https://github.com/johnlarson/blu/issues
|
|
7
|
+
Author-email: John Larson <john.robert.larson@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.14
|
|
13
|
+
Requires-Dist: aiofiles
|
|
14
|
+
Requires-Dist: uvicorn
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Blu
|
|
20
|
+
|
|
21
|
+
*Blu is still in early testing and should not be used in production applications.*
|
|
22
|
+
|
|
23
|
+
A full stack React framework for Python.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from blu import client
|
|
28
|
+
from blu.html import html, head, body, h1, button
|
|
29
|
+
|
|
30
|
+
__client__ = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def __page__():
|
|
34
|
+
return html[
|
|
35
|
+
head,
|
|
36
|
+
body[
|
|
37
|
+
h1[
|
|
38
|
+
ClickCounter
|
|
39
|
+
]
|
|
40
|
+
]
|
|
41
|
+
]
|
|
42
|
+
return ClickCounter
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@client
|
|
46
|
+
def ClickCounter():
|
|
47
|
+
count, set_count = use_state(0)
|
|
48
|
+
|
|
49
|
+
def handle_click(e):
|
|
50
|
+
set_count(count + 1)
|
|
51
|
+
|
|
52
|
+
return button(onClick=handle_click)[f"Clicks: {count}"]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install -U blu
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
Documentation available on readthedocs.io {TODO add link here.}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
blu/__init__.py,sha256=2Pjx9WUM_g5LQQnTvArVahMCslSruf4LUcr3ACrhMSM,1595
|
|
2
|
+
blu/_cli.py,sha256=DeavgvZQ1J-OFTx3YexscS99F21XN2Ju3Y-wPyWqCoo,381
|
|
3
|
+
blu/_exceptions.py,sha256=OuQXjkYQoaNqqN95Du3ZPI-cv8rLoIY7vhkfi0n2xLU,181
|
|
4
|
+
blu/_hooks.py,sha256=COw0KYo715H-7bqJmnLwtJv0FL9cJt6B7ZtTVdHH9q8,9373
|
|
5
|
+
blu/_http.py,sha256=KUJo4xaHr_cWtb9AASQHe4mdRqukWSQx0KcoJrbkQpc,6709
|
|
6
|
+
blu/_nodes.py,sha256=h_ZBJ0yz9Y4r4f5H9Ptz8oX-vMmq3NNwSiU9UuRsoio,20658
|
|
7
|
+
blu/_server_functions.py,sha256=eo6n9PGAHPf0j7S8nVzBR_LEs5YwCMmrXlkcdKKw2VM,12379
|
|
8
|
+
blu/_settings.py,sha256=N641QuUVF0gxi8lmwnUz7DgsDdCqGcVHIWY_P9wCLRU,756
|
|
9
|
+
blu/html.py,sha256=6DNA8gZvSHOnXz2LeMTpmOuSmMnpV-vH_hen68PzsjE,2015
|
|
10
|
+
blu/util.js,sha256=YKZDhi5JVbTEdUvT7NUrqP9mleSf0hdogNnoe18OxhQ,8726
|
|
11
|
+
blu/_app/__init__.py,sha256=RcbC3sJy9vqslKFD5Gn6YMay0NmOXr01WCH8m10LyJI,13497
|
|
12
|
+
blu/_app/client_main.py,sha256=eRDPsGIk5jL9JSyJsvcZCxBv33CL_JhJ5WFKYloDSzg,4526
|
|
13
|
+
blu/_app/render.py,sha256=_1co_teF1DfGrHkYCFJI8O5sRcERn3LCft5o8VIsXeY,2862
|
|
14
|
+
blu/_app/router.py,sha256=wgWFyq0Y7kgYpCsUrzAvBVOGLaOiwbqSEQMWDjtXfXg,8463
|
|
15
|
+
blu/_utils/__init__.py,sha256=jpwYug4Uo1azEq2F8rKa1QGXZDgyWlC_X3Hr11Afw1o,948
|
|
16
|
+
blu/_utils/asgi.py,sha256=pp5D7mP9PnilE7g1sGA3zRc2fDmq21Rbm-i_bNVrKw8,5214
|
|
17
|
+
blu/_utils/asyncio.py,sha256=AdDT1ZwJatjcC0uJAPxpSvCOSKw8Hg2eORe4_mvK62M,1740
|
|
18
|
+
blu/_utils/client.py,sha256=T395O9ZBIz-T9C6A7XT1kA2cstl7sivp_KWpDhVkzx8,69
|
|
19
|
+
blu/_utils/json.py,sha256=NA-HXBrnyRf9gtRePsnmNVYONxCumYHfD9JzCoFvrTE,750
|
|
20
|
+
blu/_utils/typing.py,sha256=gqFGkNlLfd5L7r3t_ldXsr7xuj5YRz7MsCCW-BqyDhA,686
|
|
21
|
+
blu_react-0.0.1.dist-info/METADATA,sha256=yl0_6kznmNfQhfD4s8vr4Kg_sfldZJXtLBtHv6Eq0PY,1252
|
|
22
|
+
blu_react-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
23
|
+
blu_react-0.0.1.dist-info/entry_points.txt,sha256=ImwX7vYP0FyTuSw1SM1BdO0JjXGHlTFGtdKY_5-tMF0,37
|
|
24
|
+
blu_react-0.0.1.dist-info/licenses/LICENSE,sha256=hy3N593ZwXshyckVnTMO0zLPijS5B6rinI-jNoA3PrE,1067
|
|
25
|
+
blu_react-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|