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/_hooks.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine, Generator
|
|
2
|
+
from typing import Any
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from blu._utils import is_client
|
|
6
|
+
|
|
7
|
+
if is_client or typing.TYPE_CHECKING:
|
|
8
|
+
from pyscript.js_modules._blu_js_utils import useEffect, useRefObj, useState
|
|
9
|
+
|
|
10
|
+
# Integers for JS `useEffect` (avoid classifying return values in JS, where
|
|
11
|
+
# coroutines can be finalized before `isinstance` runs).
|
|
12
|
+
_use_effect_ag_phase: dict[int, str] = {}
|
|
13
|
+
|
|
14
|
+
# String tags for JS (reliable compare; PyProxy int coercion was flaky).
|
|
15
|
+
_USE_EFFECT_TAG_ASYNC_GENERATOR = "async_generator"
|
|
16
|
+
_USE_EFFECT_TAG_COROUTINE = "coroutine"
|
|
17
|
+
_USE_EFFECT_TAG_GENERATOR = "generator"
|
|
18
|
+
_USE_EFFECT_TAG_PLAIN = "plain"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _unwrap_js_proxy(obj: Any) -> Any:
|
|
22
|
+
"""Resolve Pyodide ``JsProxy`` so ``id`` matches the underlying Python object."""
|
|
23
|
+
if not is_client:
|
|
24
|
+
return obj
|
|
25
|
+
try:
|
|
26
|
+
from pyodide.ffi import JsProxy
|
|
27
|
+
except ImportError:
|
|
28
|
+
return obj
|
|
29
|
+
while isinstance(obj, JsProxy):
|
|
30
|
+
obj = obj.unwrap()
|
|
31
|
+
return obj
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _use_effect_invoke(callback: Callable[[], Any]) -> tuple[str, Any]:
|
|
35
|
+
"""Call ``callback()`` and classify the return value for ``util.js`` ``useEffect``."""
|
|
36
|
+
from collections.abc import AsyncGenerator as AsyncGenABC
|
|
37
|
+
from collections.abc import Coroutine as CoroutineABC
|
|
38
|
+
from collections.abc import Generator as GenABC
|
|
39
|
+
|
|
40
|
+
result = callback()
|
|
41
|
+
if isinstance(result, AsyncGenABC):
|
|
42
|
+
_use_effect_start_async_gen(result)
|
|
43
|
+
return (_USE_EFFECT_TAG_ASYNC_GENERATOR, result)
|
|
44
|
+
if isinstance(result, CoroutineABC):
|
|
45
|
+
_use_effect_start_coro(result)
|
|
46
|
+
return (_USE_EFFECT_TAG_COROUTINE, result)
|
|
47
|
+
if isinstance(result, GenABC):
|
|
48
|
+
return (_USE_EFFECT_TAG_GENERATOR, result)
|
|
49
|
+
return (_USE_EFFECT_TAG_PLAIN, result)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _use_effect_start_async_gen(agen: AsyncGenerator[Any, None]) -> None:
|
|
53
|
+
"""Schedule running an async generator up to its first ``yield``."""
|
|
54
|
+
import asyncio
|
|
55
|
+
|
|
56
|
+
agen = _unwrap_js_proxy(agen)
|
|
57
|
+
aid = id(agen)
|
|
58
|
+
_use_effect_ag_phase[aid] = "pending"
|
|
59
|
+
|
|
60
|
+
async def setup() -> None:
|
|
61
|
+
await anext(agen)
|
|
62
|
+
_use_effect_ag_phase[aid] = "after_yield"
|
|
63
|
+
|
|
64
|
+
asyncio.create_task(setup())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _use_effect_cleanup_async_gen(
|
|
68
|
+
agen: AsyncGenerator[Any, None],
|
|
69
|
+
on_done: Callable[[], None] | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Schedule teardown or ``aclose`` for an effect async generator.
|
|
72
|
+
|
|
73
|
+
``on_done`` runs after async work finishes (e.g. to release JS proxies). The
|
|
74
|
+
client must not destroy ``agen`` until then, or the cleanup task may see a
|
|
75
|
+
dead generator.
|
|
76
|
+
"""
|
|
77
|
+
import asyncio
|
|
78
|
+
|
|
79
|
+
agen = _unwrap_js_proxy(agen)
|
|
80
|
+
|
|
81
|
+
async def work() -> None:
|
|
82
|
+
try:
|
|
83
|
+
aid = id(agen)
|
|
84
|
+
phase = _use_effect_ag_phase.pop(aid, "pending")
|
|
85
|
+
if phase == "after_yield":
|
|
86
|
+
try:
|
|
87
|
+
await anext(agen)
|
|
88
|
+
except StopAsyncIteration:
|
|
89
|
+
pass
|
|
90
|
+
else:
|
|
91
|
+
try:
|
|
92
|
+
await agen.aclose()
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
finally:
|
|
96
|
+
if on_done is not None:
|
|
97
|
+
on_done()
|
|
98
|
+
|
|
99
|
+
asyncio.create_task(work())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _use_effect_start_coro(coro: Coroutine[Any, Any, Any]) -> None:
|
|
103
|
+
import asyncio
|
|
104
|
+
|
|
105
|
+
asyncio.create_task(coro)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def use_effect(
|
|
109
|
+
callback: Callable[
|
|
110
|
+
[],
|
|
111
|
+
None
|
|
112
|
+
| Generator[None]
|
|
113
|
+
| Coroutine[None, None, None]
|
|
114
|
+
| AsyncGenerator[None, None],
|
|
115
|
+
],
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
.. include:: /_includes/hook-note.rst
|
|
119
|
+
|
|
120
|
+
Perform set-up actions immediately after a :class:`ClientElement
|
|
121
|
+
<blu.ClientElement>` is rendered to the DOM and/or tear-down actions
|
|
122
|
+
immediately before it is removed from the DOM.
|
|
123
|
+
|
|
124
|
+
.. code-block:: python
|
|
125
|
+
|
|
126
|
+
from blu import client, use_effect
|
|
127
|
+
from blu.html import div
|
|
128
|
+
|
|
129
|
+
__client__ = True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@client
|
|
133
|
+
def MyClientElement():
|
|
134
|
+
|
|
135
|
+
@use_effect
|
|
136
|
+
def setup_and_teardown():
|
|
137
|
+
do_some_setup()
|
|
138
|
+
yield
|
|
139
|
+
do_some_teardown()
|
|
140
|
+
|
|
141
|
+
return div['Hello!']
|
|
142
|
+
|
|
143
|
+
:param callback: A non-generator function or a generator function
|
|
144
|
+
with a single ``yield`` statement.
|
|
145
|
+
|
|
146
|
+
If ``callback`` is a generator function or async generator function,
|
|
147
|
+
it will be run right up until the ``yield`` statement immediately
|
|
148
|
+
after the element is initially rendered to the DOM. The rest of the
|
|
149
|
+
function will be run immediately before the element is removed from
|
|
150
|
+
the DOM.
|
|
151
|
+
|
|
152
|
+
Otherwise, ``callback`` will be called immediately after the element
|
|
153
|
+
is initially rendered to the DOM.
|
|
154
|
+
"""
|
|
155
|
+
useEffect(callback)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def use_state[T](init: T = None) -> tuple[T, Callable[[T], None]]:
|
|
159
|
+
"""
|
|
160
|
+
.. include:: /_includes/hook-note.rst
|
|
161
|
+
|
|
162
|
+
Handle state management for an element.
|
|
163
|
+
|
|
164
|
+
.. code-block:: python
|
|
165
|
+
|
|
166
|
+
from blu import client
|
|
167
|
+
from blu.html import button
|
|
168
|
+
|
|
169
|
+
__client__ = True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@client
|
|
173
|
+
def MyElement():
|
|
174
|
+
click_count, set_click_count = use_state(0)
|
|
175
|
+
|
|
176
|
+
def handle_click(e):
|
|
177
|
+
set_click_count(click_count + 1)
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
button(onClick=handle_click)[
|
|
181
|
+
f'You\\'ve clicked {click_count} times',
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
:param init: An initial value for the state being managed.
|
|
186
|
+
:return: A tuple containing two items: The current value for the
|
|
187
|
+
state being managed, and a function that takes any value and
|
|
188
|
+
causes the element whose render function ``use_state`` was
|
|
189
|
+
called in to re-render. On a re-render triggered by this setter,
|
|
190
|
+
the first item in the tuple will be whatever was passed into the
|
|
191
|
+
setter when the re-render was triggered. On a re-render not
|
|
192
|
+
triggered by this state's setter, the first item in the tuple
|
|
193
|
+
will be whatever it was in the previous render. On the initial
|
|
194
|
+
render, the first item in the tuple will be ``init``.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
return tuple(useState(init))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Ref[T]:
|
|
201
|
+
"""
|
|
202
|
+
The object returned by :func:`blu.use_ref`.
|
|
203
|
+
|
|
204
|
+
.. code-block:: python
|
|
205
|
+
|
|
206
|
+
from blu import client
|
|
207
|
+
from blu.tml import button
|
|
208
|
+
|
|
209
|
+
__client__ = True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@client
|
|
213
|
+
def TwoButtons():
|
|
214
|
+
# assigns variable ref to an instance of Ref.
|
|
215
|
+
ref = use_ref()
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
_value: T
|
|
219
|
+
_ref_proxy: Any
|
|
220
|
+
|
|
221
|
+
def __getitem__(self, empty_slice: slice) -> T:
|
|
222
|
+
"""
|
|
223
|
+
Get the value currently stored in the :class:`Ref <blu.Ref>`.
|
|
224
|
+
|
|
225
|
+
.. code-block:: python
|
|
226
|
+
|
|
227
|
+
from blu import client
|
|
228
|
+
from blu.html import button
|
|
229
|
+
|
|
230
|
+
__client__ = True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@client
|
|
234
|
+
def TwoButtons():
|
|
235
|
+
ref = use_ref('Hello')
|
|
236
|
+
ref[:] # 'Hello'
|
|
237
|
+
|
|
238
|
+
:param empty_slice: Must be an empty slice, i.e. you must put a
|
|
239
|
+
single colon between the square brackets, without any
|
|
240
|
+
numbers.
|
|
241
|
+
:return: The value currently stored in ``self``.
|
|
242
|
+
"""
|
|
243
|
+
if empty_slice.start or empty_slice.stop or empty_slice.step:
|
|
244
|
+
raise
|
|
245
|
+
return self._value
|
|
246
|
+
|
|
247
|
+
def __setitem__(self, empty_slice: slice, new_value: T):
|
|
248
|
+
"""
|
|
249
|
+
Set the :class:`Ref <blu.Ref>` to point to a different value.
|
|
250
|
+
|
|
251
|
+
.. code-block:: python
|
|
252
|
+
|
|
253
|
+
from blu import client
|
|
254
|
+
from blu.tml import button
|
|
255
|
+
|
|
256
|
+
__client__ = True
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@client
|
|
260
|
+
def TwoButtons():
|
|
261
|
+
ref = use_ref('Hello')
|
|
262
|
+
ref[:] = 'Goodbye'
|
|
263
|
+
ref[:] # 'Goodbye'
|
|
264
|
+
|
|
265
|
+
:param empty_slice: Must be an empty slice, i.e. you must put a
|
|
266
|
+
single colon between the square brackets, without any
|
|
267
|
+
numbers.
|
|
268
|
+
:param new_value: The new value that should be stored in
|
|
269
|
+
``self``.
|
|
270
|
+
|
|
271
|
+
Once this method has been called, the :class:`Ref <blu.Ref>`\\'s
|
|
272
|
+
stored value will be set to ``new_value``.
|
|
273
|
+
"""
|
|
274
|
+
if empty_slice.start or empty_slice.stop or empty_slice.step:
|
|
275
|
+
raise
|
|
276
|
+
self._value = new_value
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def use_ref[T](init: T = None) -> Ref[T]:
|
|
280
|
+
"""
|
|
281
|
+
.. include:: /_includes/hook-note.rst
|
|
282
|
+
|
|
283
|
+
Store a value that doesn't change between renders unless explicitly
|
|
284
|
+
set to a new value.
|
|
285
|
+
|
|
286
|
+
.. code-block:: python
|
|
287
|
+
|
|
288
|
+
from blu import client, is_client, use_ref
|
|
289
|
+
from blu.html import button
|
|
290
|
+
|
|
291
|
+
if is_client:
|
|
292
|
+
from js import alert
|
|
293
|
+
|
|
294
|
+
__client__ = True
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@client
|
|
298
|
+
def MyElement():
|
|
299
|
+
click_count_ref = use_ref(0)
|
|
300
|
+
|
|
301
|
+
def handle_click(e):
|
|
302
|
+
click_count_ref[:] = click_count_ref[:] + 1
|
|
303
|
+
count = click_count_ref[:]
|
|
304
|
+
alert(f'You\\'ve clicked the button {count} times.')
|
|
305
|
+
|
|
306
|
+
return button['Click me!']
|
|
307
|
+
|
|
308
|
+
:param init: Any value.
|
|
309
|
+
:return: A :class:`Ref <blu.Ref>`. This will be the same
|
|
310
|
+
:class:`Ref <blu.Ref>` object on every render of the same
|
|
311
|
+
element. On the initial render, the value stored in the
|
|
312
|
+
:class:`Ref <blu.Ref>` will be ``init``, but you can set this to
|
|
313
|
+
a new value at any time (see :class:`blu.Ref` for how to set a
|
|
314
|
+
:class:`Ref <blu.Ref>`\\'s value).
|
|
315
|
+
|
|
316
|
+
.. note::
|
|
317
|
+
|
|
318
|
+
Setting a :class:`Ref <blu.Ref>`\\'s value does not trigger a
|
|
319
|
+
re-render.
|
|
320
|
+
"""
|
|
321
|
+
ref: Ref[T] = Ref()
|
|
322
|
+
ref[:] = init
|
|
323
|
+
return useRefObj(ref)
|
blu/_http.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
import urllib.parse
|
|
3
|
+
|
|
4
|
+
from blu._nodes import Node
|
|
5
|
+
from blu._utils.typing import Iterator, Mapping
|
|
6
|
+
|
|
7
|
+
type QueryInit = Mapping[str, str | list[str]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QueryParams:
|
|
11
|
+
"""
|
|
12
|
+
The query parameters in a request URL.
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
from blu import Request
|
|
17
|
+
|
|
18
|
+
request = Request('GET', '/?my-query-param=my-value')
|
|
19
|
+
|
|
20
|
+
request.query().get('my-query-param') == 'my-value'
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_items: dict[str, list[str]]
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_query_string(cls, query_string: str) -> "QueryParams":
|
|
27
|
+
"""
|
|
28
|
+
Parse a query string.
|
|
29
|
+
|
|
30
|
+
:param query_string: The query string to parse, without the
|
|
31
|
+
leading ``?`` character.
|
|
32
|
+
|
|
33
|
+
:return: The :class:`QueryParams` representation of the query
|
|
34
|
+
string.
|
|
35
|
+
"""
|
|
36
|
+
pair_strings = query_string.split("&") if query_string else []
|
|
37
|
+
pairs = [x.split("=") for x in pair_strings]
|
|
38
|
+
query_params_init: dict[str, list[str]] = defaultdict(lambda: [])
|
|
39
|
+
for k, v in pairs:
|
|
40
|
+
decoded = urllib.parse.unquote_plus(v)
|
|
41
|
+
query_params_init[k].append(decoded)
|
|
42
|
+
return QueryParams({**query_params_init})
|
|
43
|
+
|
|
44
|
+
def __init__(self, items: QueryInit):
|
|
45
|
+
"""
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
# ?sortBy='relevance&q=position&q=velocity&q=acceleration
|
|
49
|
+
QueryParams({
|
|
50
|
+
'sortBy': 'relevance',
|
|
51
|
+
'q': ['position', 'velocity', 'acceleration'],
|
|
52
|
+
})
|
|
53
|
+
:param items: A mapping of query parameter names to values. If
|
|
54
|
+
a query parameter has only one value, then its value can be
|
|
55
|
+
represented as a :py:class:`str`. If it has more than one
|
|
56
|
+
value, it should be represented as a
|
|
57
|
+
:py:class:`list` of :py:class:`str`\\ s.
|
|
58
|
+
"""
|
|
59
|
+
self._items = {k: v if isinstance(v, list) else [v] for k, v in items.items()}
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, key: str) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Get the value of a query parameter.
|
|
64
|
+
|
|
65
|
+
.. code-block:: python
|
|
66
|
+
|
|
67
|
+
QueryParams.from_query_string('x=value')['x'] == 'value'
|
|
68
|
+
|
|
69
|
+
:param key: A query parameter name.
|
|
70
|
+
|
|
71
|
+
:return: The value of that query parameter. If the parameter has
|
|
72
|
+
multiple values (e.g. ``a=1&1=34``), returns the first.
|
|
73
|
+
|
|
74
|
+
:raise KeyError: If the :class:`QueryParams` object has no
|
|
75
|
+
query parameter with the name specified by :data:`key`.
|
|
76
|
+
"""
|
|
77
|
+
return self._items[key][0]
|
|
78
|
+
|
|
79
|
+
def get[T](self, key: str, default: T = None) -> str | T:
|
|
80
|
+
"""
|
|
81
|
+
Get the value of a query parameter.
|
|
82
|
+
|
|
83
|
+
.. code-block:: python
|
|
84
|
+
|
|
85
|
+
QueryParams.from_query_string('a=1').get('a') == '1'
|
|
86
|
+
|
|
87
|
+
:param key: The name of a query parameter.
|
|
88
|
+
:param default: A default value.
|
|
89
|
+
|
|
90
|
+
:return: The value of the query parameter named **key** if it
|
|
91
|
+
exists. Otherwise, **default**. If there are multiple values
|
|
92
|
+
associated with **key**, returns the first.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
return self._items[key][0]
|
|
96
|
+
except KeyError:
|
|
97
|
+
return default
|
|
98
|
+
|
|
99
|
+
def all(self, key: str) -> list[str]:
|
|
100
|
+
"""
|
|
101
|
+
Get all values associated with a query parameter key.
|
|
102
|
+
|
|
103
|
+
.. code-block:: python
|
|
104
|
+
|
|
105
|
+
q = QueryParams.from_query_string('a=1&a=2')
|
|
106
|
+
q.all('a') == ['1', '2']
|
|
107
|
+
|
|
108
|
+
:param key: A query parameter key.
|
|
109
|
+
|
|
110
|
+
:return: A :py:class:`list` of all values associated with the
|
|
111
|
+
given query parameter key, in the order they appear in the
|
|
112
|
+
query string, or, if the :class:`QueryParams` object was
|
|
113
|
+
not created from a query string, the order they were defined
|
|
114
|
+
in the data passed into the :class:`QueryParams`
|
|
115
|
+
constructor.
|
|
116
|
+
"""
|
|
117
|
+
return self._items.get(key, [])
|
|
118
|
+
|
|
119
|
+
def __iter__(self) -> Iterator[tuple[str, list[str]]]:
|
|
120
|
+
"""
|
|
121
|
+
Iterate over the keys and value lists of ``self``.
|
|
122
|
+
|
|
123
|
+
.. code-block:: python
|
|
124
|
+
|
|
125
|
+
q = QueryParams.from_query_string('a=1&b=2&b=3')
|
|
126
|
+
for name, values in q:
|
|
127
|
+
print(f'{name}:', ', '.join(values))
|
|
128
|
+
|
|
129
|
+
.. code-block:: console
|
|
130
|
+
|
|
131
|
+
a: 1
|
|
132
|
+
b: 2, 3
|
|
133
|
+
|
|
134
|
+
:return: An iterator of key-value pairs, where the key is a
|
|
135
|
+
query parameter name, and the value is a list of all string
|
|
136
|
+
values associated with that name in the query string. Pairs
|
|
137
|
+
occur in the order of names' first appearance in the query
|
|
138
|
+
string, and query values within a pair are in the order in
|
|
139
|
+
which they are set in the query string.
|
|
140
|
+
"""
|
|
141
|
+
return ((k, v) for k, v in self._items.items())
|
|
142
|
+
|
|
143
|
+
def to_query_string(self) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Serialize ``self`` to a query string.
|
|
146
|
+
|
|
147
|
+
.. code-block:: python
|
|
148
|
+
|
|
149
|
+
q = QueryParams.from_query_string('a=1&b=2')
|
|
150
|
+
q.to_query_string() == 'a=1&b=2'
|
|
151
|
+
|
|
152
|
+
:return: The query string representation of the query parameters
|
|
153
|
+
in ``self``.
|
|
154
|
+
"""
|
|
155
|
+
pairs: list[str] = []
|
|
156
|
+
for k, vals in self:
|
|
157
|
+
for v in vals:
|
|
158
|
+
pairs.append(f"{k}={v}")
|
|
159
|
+
return "&".join(pairs)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Request:
|
|
163
|
+
method: str
|
|
164
|
+
path: str
|
|
165
|
+
query: QueryParams
|
|
166
|
+
headers: dict[str, str]
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
path: str,
|
|
171
|
+
query: QueryParams | QueryInit = QueryParams({}),
|
|
172
|
+
headers: Mapping[str, str] = {},
|
|
173
|
+
):
|
|
174
|
+
self.path = path
|
|
175
|
+
if isinstance(query, QueryParams):
|
|
176
|
+
self.query = query
|
|
177
|
+
else:
|
|
178
|
+
self.query = QueryParams(query)
|
|
179
|
+
self.headers = {**headers}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Response:
|
|
183
|
+
"""
|
|
184
|
+
An HTTP response for a page in a web application. Return a
|
|
185
|
+
:class:`Response <blu.Response>` from a ``__page__`` function (see
|
|
186
|
+
:ref:`File Conventions`) to set HTTP status and headers for that
|
|
187
|
+
page.
|
|
188
|
+
|
|
189
|
+
.. code-block:: python
|
|
190
|
+
:caption: __index__.py
|
|
191
|
+
|
|
192
|
+
def __page__():
|
|
193
|
+
return Response(
|
|
194
|
+
p['Hello.'],
|
|
195
|
+
status=404,
|
|
196
|
+
headers={
|
|
197
|
+
'Cache-Control': 'no-cache',
|
|
198
|
+
'Last-Modified': 'Tue, 10 Dec 2024 10:00:00 GMT',
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
:param body: The React node to render when displaying the page.
|
|
203
|
+
:param status: The status code to send in the HTTP response.
|
|
204
|
+
:param headers: The HTTP headers to send in the HTTP response.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
_body: Node
|
|
208
|
+
_status: int
|
|
209
|
+
_headers: dict[str, str]
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
body: Node = None,
|
|
214
|
+
status: int = 200,
|
|
215
|
+
headers: Mapping[str, str] = {},
|
|
216
|
+
):
|
|
217
|
+
self._body = body
|
|
218
|
+
self._status = status
|
|
219
|
+
self._headers = {**headers}
|