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/_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
@@ -0,0 +1,3 @@
1
+ import platform
2
+
3
+ is_client: bool = platform.system() == "Emscripten"
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blu = blu._cli:cli
@@ -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.