fastlifeweb 0.16.3__py3-none-any.whl → 0.17.0__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.
- fastlife/adapters/jinjax/renderer.py +49 -25
- fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
- fastlife/adapters/jinjax/widget_factory/base.py +38 -0
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
- fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
- fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
- fastlife/adapters/jinjax/widgets/base.py +6 -4
- fastlife/adapters/jinjax/widgets/checklist.py +1 -1
- fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
- fastlife/adapters/jinjax/widgets/hidden.py +2 -0
- fastlife/adapters/jinjax/widgets/model.py +4 -1
- fastlife/adapters/jinjax/widgets/sequence.py +3 -2
- fastlife/adapters/jinjax/widgets/text.py +9 -10
- fastlife/adapters/jinjax/widgets/union.py +9 -7
- fastlife/components/Form.jinja +12 -0
- fastlife/config/configurator.py +23 -24
- fastlife/config/exceptions.py +4 -1
- fastlife/config/openapiextra.py +1 -0
- fastlife/config/resources.py +26 -27
- fastlife/config/settings.py +2 -0
- fastlife/config/views.py +3 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
- fastlife/middlewares/session/middleware.py +2 -2
- fastlife/middlewares/session/serializer.py +6 -5
- fastlife/request/form.py +7 -6
- fastlife/request/form_data.py +2 -6
- fastlife/routing/route.py +3 -1
- fastlife/routing/router.py +1 -0
- fastlife/security/csrf.py +2 -1
- fastlife/security/policy.py +2 -1
- fastlife/services/locale_negociator.py +2 -1
- fastlife/services/policy.py +3 -2
- fastlife/services/templates.py +2 -1
- fastlife/services/translations.py +15 -8
- fastlife/shared_utils/infer.py +4 -3
- fastlife/shared_utils/resolver.py +64 -4
- fastlife/templates/binding.py +2 -1
- fastlife/testing/__init__.py +1 -0
- fastlife/testing/dom.py +140 -0
- fastlife/testing/form.py +204 -0
- fastlife/testing/session.py +67 -0
- fastlife/testing/testclient.py +7 -390
- fastlife/views/pydantic_form.py +4 -4
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
- fastlife/adapters/jinjax/widgets/factory.py +0 -525
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
fastlife/testing/testclient.py
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
"""Testing your application."""
|
2
2
|
|
3
|
-
import
|
4
|
-
import
|
5
|
-
from collections.abc import MutableMapping
|
6
|
-
from http.cookiejar import Cookie
|
7
|
-
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
4
|
+
from typing import Any, Literal
|
8
5
|
from urllib.parse import urlencode
|
9
6
|
|
10
7
|
import bs4
|
@@ -16,336 +13,14 @@ from starlette.types import ASGIApp
|
|
16
13
|
from fastlife.config.settings import Settings
|
17
14
|
from fastlife.middlewares.session.serializer import AbsractSessionSerializer
|
18
15
|
from fastlife.shared_utils.resolver import resolve
|
16
|
+
from fastlife.testing.dom import Element
|
17
|
+
from fastlife.testing.form import WebForm
|
18
|
+
from fastlife.testing.session import Session
|
19
19
|
|
20
20
|
CookieTypes = httpx._types.CookieTypes # type: ignore
|
21
21
|
Cookies = httpx._models.Cookies # type: ignore
|
22
22
|
|
23
23
|
|
24
|
-
class Element:
|
25
|
-
"""Access to a dom element."""
|
26
|
-
|
27
|
-
def __init__(self, client: "WebTestClient", tag: bs4.Tag):
|
28
|
-
self._client = client
|
29
|
-
self._tag = tag
|
30
|
-
|
31
|
-
def click(self) -> "WebResponse":
|
32
|
-
"""Simulate a client to a a link. No javascript exectuted here."""
|
33
|
-
return self._client.get(self._tag.attrs["href"])
|
34
|
-
|
35
|
-
@property
|
36
|
-
def node_name(self) -> str:
|
37
|
-
"""Get the node name of the dom element."""
|
38
|
-
return self._tag.name
|
39
|
-
|
40
|
-
@property
|
41
|
-
def attrs(self) -> dict[str, str]:
|
42
|
-
"""Attributes of the element."""
|
43
|
-
return self._tag.attrs
|
44
|
-
|
45
|
-
@property
|
46
|
-
def text(self) -> str:
|
47
|
-
"""
|
48
|
-
Return the text of the element, with text of childs element.
|
49
|
-
|
50
|
-
Note that the text is stripped for convenience but inner text may contains
|
51
|
-
many spaces not manipulated here.
|
52
|
-
"""
|
53
|
-
return self._tag.text.strip()
|
54
|
-
|
55
|
-
@property
|
56
|
-
def h1(self) -> "Element":
|
57
|
-
"""
|
58
|
-
Return the h1 child element.
|
59
|
-
|
60
|
-
Should be used on the html body element directly.
|
61
|
-
"""
|
62
|
-
nodes = self.by_node_name("h1")
|
63
|
-
assert len(nodes) == 1, f"Should have 1 <h1>, got {len(nodes)} in {self}"
|
64
|
-
return nodes[0]
|
65
|
-
|
66
|
-
@property
|
67
|
-
def h2(self) -> Sequence["Element"]:
|
68
|
-
"""
|
69
|
-
Return the h2 elements.
|
70
|
-
"""
|
71
|
-
return self.by_node_name("h2")
|
72
|
-
|
73
|
-
@property
|
74
|
-
def form(self) -> "Element | None":
|
75
|
-
"""Get the form element of the web page."""
|
76
|
-
return Element(self._client, self._tag.form) if self._tag.form else None
|
77
|
-
|
78
|
-
@property
|
79
|
-
def hx_target(self) -> Optional[str]:
|
80
|
-
"""
|
81
|
-
Return the hx-target of the element.
|
82
|
-
|
83
|
-
It may be set on a parent. It also resolve special case "this" and return the id
|
84
|
-
of the element.
|
85
|
-
"""
|
86
|
-
el: bs4.Tag | None = self._tag
|
87
|
-
while el:
|
88
|
-
if "hx-target" in el.attrs:
|
89
|
-
ret = el.attrs["hx-target"]
|
90
|
-
if ret == "this":
|
91
|
-
ret = el.attrs["id"]
|
92
|
-
return ret
|
93
|
-
el = el.parent
|
94
|
-
return None
|
95
|
-
|
96
|
-
def by_text(self, text: str, *, node_name: str | None = None) -> "Element | None":
|
97
|
-
"""Find the first element that match the text."""
|
98
|
-
nodes = self.iter_all_by_text(text, node_name=node_name)
|
99
|
-
return next(nodes, None)
|
100
|
-
|
101
|
-
def iter_all_by_text(
|
102
|
-
self, text: str, *, node_name: str | None = None
|
103
|
-
) -> "Iterator[Element]":
|
104
|
-
"""Return an iterator of all elements that match the text."""
|
105
|
-
nodes = self._tag.find_all(string=re.compile(rf"\s*{text}\s*"))
|
106
|
-
for node in nodes:
|
107
|
-
if isinstance(node, bs4.NavigableString):
|
108
|
-
node = node.parent
|
109
|
-
|
110
|
-
if node_name:
|
111
|
-
while node is not None:
|
112
|
-
if node.name == node_name:
|
113
|
-
yield Element(self._client, node)
|
114
|
-
node = node.parent
|
115
|
-
elif node:
|
116
|
-
yield Element(self._client, node)
|
117
|
-
return None
|
118
|
-
|
119
|
-
def get_all_by_text(
|
120
|
-
self, text: str, *, node_name: str | None = None
|
121
|
-
) -> "Sequence[Element]":
|
122
|
-
"""Return the list of all elements that match the text."""
|
123
|
-
nodes = self.iter_all_by_text(text, node_name=node_name)
|
124
|
-
return list(nodes)
|
125
|
-
|
126
|
-
def by_label_text(self, text: str) -> "Element | None":
|
127
|
-
"""Return the element which is the target of the label having the given text."""
|
128
|
-
label = self.by_text(text, node_name="label")
|
129
|
-
assert label is not None
|
130
|
-
assert label.attrs.get("for") is not None
|
131
|
-
resp = self._tag.find(id=label.attrs["for"])
|
132
|
-
assert not isinstance(resp, bs4.NavigableString)
|
133
|
-
return Element(self._client, resp) if resp else None
|
134
|
-
|
135
|
-
def by_node_name(
|
136
|
-
self, node_name: str, *, attrs: dict[str, str] | None = None
|
137
|
-
) -> list["Element"]:
|
138
|
-
"""
|
139
|
-
Return the list of elements with the given node_name.
|
140
|
-
|
141
|
-
An optional set of attributes may given and must match if passed.
|
142
|
-
"""
|
143
|
-
return [
|
144
|
-
Element(self._client, e) for e in self._tag.find_all(node_name, attrs or {})
|
145
|
-
]
|
146
|
-
|
147
|
-
def __repr__(self) -> str:
|
148
|
-
return f"<{self.node_name}>"
|
149
|
-
|
150
|
-
def __str__(self) -> str:
|
151
|
-
return str(self._tag)
|
152
|
-
|
153
|
-
|
154
|
-
class WebForm:
|
155
|
-
"""
|
156
|
-
Handle html form.
|
157
|
-
|
158
|
-
Form are filled out and submit with methods and try to avoid invalid
|
159
|
-
usage, such as selecting an option that don't exists is not possible here.
|
160
|
-
Again, no javascript is executed here, but htmx attribute `hx-post` and `hx-target`
|
161
|
-
are read while submiting to simulate it.
|
162
|
-
"""
|
163
|
-
|
164
|
-
def __init__(self, client: "WebTestClient", origin: str, form: Element):
|
165
|
-
self._client = client
|
166
|
-
self._form = form
|
167
|
-
self._origin = origin
|
168
|
-
self._formfields: dict[str, Element] = {}
|
169
|
-
self._formdata: MultiDict[str] = MultiDict()
|
170
|
-
inputs = self._form.by_node_name("input")
|
171
|
-
for input in inputs:
|
172
|
-
self._formfields[input.attrs["name"]] = input
|
173
|
-
if input.attrs.get("type") == "checkbox" and "checked" not in input.attrs:
|
174
|
-
continue
|
175
|
-
self._formdata.add(input.attrs["name"], input.attrs.get("value", ""))
|
176
|
-
|
177
|
-
selects = self._form.by_node_name("select")
|
178
|
-
for select in selects:
|
179
|
-
fieldname = select.attrs["name"]
|
180
|
-
self._formfields[fieldname] = select
|
181
|
-
options = select.by_node_name("option")
|
182
|
-
if "multiple" in select.attrs:
|
183
|
-
for option in options:
|
184
|
-
if "selected" in option.attrs:
|
185
|
-
self._formdata.add(
|
186
|
-
fieldname, option.attrs.get("value", option.text)
|
187
|
-
)
|
188
|
-
else:
|
189
|
-
if options:
|
190
|
-
self._formdata[fieldname] = options[0].attrs.get(
|
191
|
-
"value", options[0].text
|
192
|
-
)
|
193
|
-
for option in options:
|
194
|
-
if "selected" in option.attrs:
|
195
|
-
self._formdata[fieldname] = option.attrs.get(
|
196
|
-
"value", option.text
|
197
|
-
)
|
198
|
-
break
|
199
|
-
|
200
|
-
# field textearea...
|
201
|
-
|
202
|
-
def set(self, fieldname: str, value: str) -> Any:
|
203
|
-
"""
|
204
|
-
Set a value to an input field.
|
205
|
-
|
206
|
-
It works for checkbox and radio as well.
|
207
|
-
Checkbox may contains many values.
|
208
|
-
Options of select can't be set with this method, the select method must
|
209
|
-
be used instead.
|
210
|
-
"""
|
211
|
-
if fieldname not in self._formfields:
|
212
|
-
raise ValueError(f'"{fieldname}" does not exists')
|
213
|
-
if self._formfields[fieldname].node_name == "select":
|
214
|
-
raise ValueError(f'"{fieldname}" is a <select>, use select() instead')
|
215
|
-
|
216
|
-
if self._formfields[fieldname].attrs.get("type") == "checkbox":
|
217
|
-
self._formdata.add(fieldname, value)
|
218
|
-
return
|
219
|
-
|
220
|
-
if self._formfields[fieldname].attrs.get("type") == "radio":
|
221
|
-
radio = self._form.by_node_name(
|
222
|
-
"input", attrs={"type": "radio", "value": value}
|
223
|
-
)
|
224
|
-
if not radio:
|
225
|
-
raise ValueError(
|
226
|
-
f'radio "{fieldname}" does not contains {value} option'
|
227
|
-
)
|
228
|
-
|
229
|
-
self._formdata[fieldname] = value
|
230
|
-
|
231
|
-
def unset(self, fieldname: str, value: str) -> Any:
|
232
|
-
"""Unset an element. Only works with checkbox."""
|
233
|
-
if fieldname not in self._formfields:
|
234
|
-
raise ValueError(f'"{fieldname}" does not exists')
|
235
|
-
if self._formfields[fieldname].node_name != "input":
|
236
|
-
raise ValueError(f'"{fieldname}" is not a checkbox')
|
237
|
-
if self._formfields[fieldname].attrs.get("type") != "checkbox":
|
238
|
-
raise ValueError(f'"{fieldname}" is not a checkbox')
|
239
|
-
values = self._formdata.popall(fieldname)
|
240
|
-
if value not in values:
|
241
|
-
raise ValueError(f'"{value}" not in "{fieldname}"')
|
242
|
-
for val in values:
|
243
|
-
if val != value:
|
244
|
-
self._formdata[fieldname] = val
|
245
|
-
|
246
|
-
def select(self, fieldname: str, value: str) -> Any:
|
247
|
-
"""
|
248
|
-
Select an option, if multiple, value is added, otherwise, value is replaced.
|
249
|
-
"""
|
250
|
-
if fieldname not in self._formfields:
|
251
|
-
raise ValueError(f'"{fieldname}" does not exists')
|
252
|
-
field = self._formfields[fieldname]
|
253
|
-
if field.node_name != "select":
|
254
|
-
raise ValueError(f"{fieldname} is a {repr(field)}, " "use set() instead")
|
255
|
-
|
256
|
-
for option in field.by_node_name("option"):
|
257
|
-
if option.text == value.strip():
|
258
|
-
if "multiple" in field.attrs:
|
259
|
-
self._formdata.add(fieldname, value)
|
260
|
-
else:
|
261
|
-
self._formdata[fieldname] = option.attrs.get("value", option.text)
|
262
|
-
break
|
263
|
-
else:
|
264
|
-
raise ValueError(f'No option {value} in <select name="{fieldname}">')
|
265
|
-
|
266
|
-
def unselect(self, fieldname: str, value: str) -> Any:
|
267
|
-
"""
|
268
|
-
Unselect an option if multiple, otherwise an exception is raised.
|
269
|
-
"""
|
270
|
-
if fieldname not in self._formfields:
|
271
|
-
raise ValueError(f'"{fieldname}" does not exists')
|
272
|
-
field = self._formfields[fieldname]
|
273
|
-
|
274
|
-
if field.node_name != "select":
|
275
|
-
raise ValueError(
|
276
|
-
f"{fieldname} is a {repr(self._formfields[fieldname])}, "
|
277
|
-
"use unset() for checkbox instead"
|
278
|
-
)
|
279
|
-
if "multiple" not in field.attrs:
|
280
|
-
raise ValueError("only <select multiple> support unselect")
|
281
|
-
|
282
|
-
for option in self._formfields[fieldname].by_node_name("option"):
|
283
|
-
if option.text == value.strip():
|
284
|
-
values = self._formdata.popall(fieldname)
|
285
|
-
if value not in values:
|
286
|
-
raise ValueError(f'"{value}" not selected in "{fieldname}"')
|
287
|
-
for val in values:
|
288
|
-
if val != value:
|
289
|
-
self._formdata[fieldname] = val
|
290
|
-
break
|
291
|
-
else:
|
292
|
-
raise ValueError(f'No option {value} in <select name="{fieldname}">')
|
293
|
-
|
294
|
-
def button(self, text: str, position: int = 0) -> "WebForm":
|
295
|
-
"""
|
296
|
-
Simmulate a click on a button using the text of the button,
|
297
|
-
|
298
|
-
and eventually a position. The button return the form and the submit()
|
299
|
-
should be called directly.
|
300
|
-
|
301
|
-
This is used in order to inject the value of the button in the form, usually
|
302
|
-
done while many actions are available on a form.
|
303
|
-
|
304
|
-
::
|
305
|
-
|
306
|
-
form.button("Go").submit()
|
307
|
-
|
308
|
-
"""
|
309
|
-
buttons = self._form.get_all_by_text(text, node_name="button")
|
310
|
-
if position >= len(buttons):
|
311
|
-
pos = ""
|
312
|
-
if position > 0:
|
313
|
-
pos = f" at position {position}"
|
314
|
-
raise ValueError(f'Button "{text}" not found{pos}')
|
315
|
-
button = buttons[position]
|
316
|
-
if "name" in button.attrs:
|
317
|
-
self._formdata[button.attrs["name"]] = button.attrs.get("value", "")
|
318
|
-
return self
|
319
|
-
|
320
|
-
def submit(self, follow_redirects: bool = True) -> "WebResponse":
|
321
|
-
"""
|
322
|
-
Submit the form as it has been previously filled out.
|
323
|
-
"""
|
324
|
-
headers: dict[str, str] = {}
|
325
|
-
target = (
|
326
|
-
self._form.attrs.get("hx-post")
|
327
|
-
or self._form.attrs.get("post")
|
328
|
-
or self._origin
|
329
|
-
)
|
330
|
-
if "hx-post" in self._form.attrs:
|
331
|
-
if hx_target := self._form.hx_target:
|
332
|
-
headers["HX-Target"] = hx_target
|
333
|
-
|
334
|
-
return self._client.post(
|
335
|
-
target,
|
336
|
-
data=self._formdata,
|
337
|
-
headers=headers,
|
338
|
-
follow_redirects=follow_redirects,
|
339
|
-
)
|
340
|
-
|
341
|
-
def __contains__(self, key: str) -> bool:
|
342
|
-
"""Test if a field exists in the form."""
|
343
|
-
return key in self._formdata
|
344
|
-
|
345
|
-
def __repr__(self) -> str:
|
346
|
-
return repr(self._formdata)
|
347
|
-
|
348
|
-
|
349
24
|
class WebResponse:
|
350
25
|
"""Represent an http response made by the WebTestClient browser."""
|
351
26
|
|
@@ -392,7 +67,7 @@ class WebResponse:
|
|
392
67
|
def html_body(self) -> Element:
|
393
68
|
"""The body element of the html response."""
|
394
69
|
body = self.html.by_node_name("body")
|
395
|
-
assert len(body) == 1
|
70
|
+
assert len(body) == 1, "body element not found or multiple body found"
|
396
71
|
return body[0]
|
397
72
|
|
398
73
|
@property
|
@@ -400,7 +75,7 @@ class WebResponse:
|
|
400
75
|
"""The form element of the html response."""
|
401
76
|
if self._form is None:
|
402
77
|
form = self.html.form
|
403
|
-
assert form is not None
|
78
|
+
assert form is not None, "form element not found"
|
404
79
|
self._form = WebForm(self._client, self._origin, form)
|
405
80
|
return self._form
|
406
81
|
|
@@ -419,64 +94,6 @@ class WebResponse:
|
|
419
94
|
return self.html.by_node_name(node_name, attrs=attrs)
|
420
95
|
|
421
96
|
|
422
|
-
class Session(dict[str, Any]):
|
423
|
-
"""Manipulate the session of the WebTestClient browser."""
|
424
|
-
|
425
|
-
def __init__(self, client: "WebTestClient"):
|
426
|
-
self.client = client
|
427
|
-
self.srlz = client.session_serializer
|
428
|
-
self.settings = self.client.settings
|
429
|
-
data: Mapping[str, Any]
|
430
|
-
cookie_name = self.settings.session_cookie_name
|
431
|
-
self.has_session = cookie_name in self.client.cookies
|
432
|
-
if self.has_session:
|
433
|
-
data, error = self.srlz.deserialize(
|
434
|
-
self.client.cookies[cookie_name].encode("utf-8")
|
435
|
-
)
|
436
|
-
if error:
|
437
|
-
self.has_session = False
|
438
|
-
else:
|
439
|
-
data = {}
|
440
|
-
super().__init__(data)
|
441
|
-
|
442
|
-
def __setitem__(self, __key: Any, __value: Any) -> None:
|
443
|
-
"""Initialize a value in the session of the client in order to test."""
|
444
|
-
super().__setitem__(__key, __value)
|
445
|
-
settings = self.settings
|
446
|
-
data = self.serialize()
|
447
|
-
self.client.cookies.jar.set_cookie(
|
448
|
-
Cookie(
|
449
|
-
version=0,
|
450
|
-
name=settings.session_cookie_name,
|
451
|
-
value=data,
|
452
|
-
port=None,
|
453
|
-
port_specified=False,
|
454
|
-
domain=f".{settings.session_cookie_domain}",
|
455
|
-
domain_specified=True,
|
456
|
-
domain_initial_dot=True,
|
457
|
-
path="/",
|
458
|
-
path_specified=True,
|
459
|
-
secure=False,
|
460
|
-
expires=int(time.time() + settings.session_duration.total_seconds()),
|
461
|
-
discard=False,
|
462
|
-
comment=None,
|
463
|
-
comment_url=None,
|
464
|
-
rest={"HttpOnly": None, "SameSite": "lax"}, # type: ignore
|
465
|
-
rfc2109=False,
|
466
|
-
)
|
467
|
-
)
|
468
|
-
# this does not work
|
469
|
-
# self.client.cookies.set(
|
470
|
-
# settings.session_cookie_name,
|
471
|
-
# data,
|
472
|
-
# settings.session_cookie_domain,
|
473
|
-
# settings.session_cookie_path,
|
474
|
-
# )
|
475
|
-
|
476
|
-
def serialize(self) -> str:
|
477
|
-
return self.srlz.serialize(self).decode("utf-8")
|
478
|
-
|
479
|
-
|
480
97
|
class WebTestClient:
|
481
98
|
"""The fake browser used for testing purpose."""
|
482
99
|
|
fastlife/views/pydantic_form.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import cast
|
2
2
|
|
3
3
|
from fastapi import Query
|
4
4
|
from pydantic.fields import FieldInfo
|
@@ -11,9 +11,9 @@ from fastlife.shared_utils.resolver import resolve_extended
|
|
11
11
|
async def show_widget(
|
12
12
|
typ: str,
|
13
13
|
request: Request,
|
14
|
-
title:
|
15
|
-
name:
|
16
|
-
token:
|
14
|
+
title: str | None = Query(None),
|
15
|
+
name: str | None = Query(None),
|
16
|
+
token: str | None = Query(None),
|
17
17
|
removable: bool = Query(False),
|
18
18
|
) -> Response:
|
19
19
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastlifeweb
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.17.0
|
4
4
|
Summary: High-level web framework
|
5
5
|
Home-page: https://github.com/mardiros/fastlife
|
6
6
|
License: BSD-derived
|
@@ -37,24 +37,24 @@ Description-Content-Type: text/markdown
|
|
37
37
|
[](https://mardiros.github.io/fastlife/)
|
38
38
|
[](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
|
39
39
|
[](https://codecov.io/gh/mardiros/fastlife)
|
40
|
-
|
40
|
+
[](https://codeclimate.com/github/mardiros/fastlife/maintainability)
|
41
41
|
|
42
42
|
> ⚠️ **Under Heavy Development**
|
43
43
|
> Please note that this project is still in active development. Features and APIs may change frequently.
|
44
44
|
> Even the name is not definitive.
|
45
45
|
|
46
|
-
An opinionated Python web framework (based on FastAPI).
|
46
|
+
An opinionated Python web framework (based on {term}`FastAPI`).
|
47
47
|
|
48
48
|
## Purpose
|
49
49
|
|
50
50
|
Fastlife helps at building Web Application with session, security, html test client,
|
51
51
|
and html form generated from pydantic schema using customizable widget.
|
52
52
|
|
53
|
-
Templates are made using
|
53
|
+
Templates are made using {term}`JinjaX` and an extensible [set of
|
54
54
|
component](https://mardiros.github.io/fastlife/components/index.html) is available
|
55
55
|
in order to build pages.
|
56
56
|
|
57
|
-
Those components are currently stylized by
|
57
|
+
Those components are currently stylized by {term}`Tailwind CSS`,
|
58
58
|
using [pytailwindcss](https://github.com/timonweb/pytailwindcss).
|
59
59
|
|
60
60
|
Moreover, you can also write API, in an opinionated way to enforce documentation
|
@@ -63,7 +63,7 @@ consistency.
|
|
63
63
|
|
64
64
|
## First class configuration.
|
65
65
|
|
66
|
-
Fastlife is adding a "Configurator", like Pyramid to get a better scallable codebase.
|
66
|
+
Fastlife is adding a "Configurator", like {term}`Pyramid` to get a better scallable codebase.
|
67
67
|
|
68
68
|
The configurator in fastlife organizes configuration settings hierarchically,
|
69
69
|
enabling easy management and overriding at different levels.
|
@@ -1,24 +1,36 @@
|
|
1
1
|
fastlife/__init__.py,sha256=fokakuhI0fdAjHP5w6GWi-YfCx7iTnrVzjSyZ11Cdgg,676
|
2
2
|
fastlife/adapters/__init__.py,sha256=WYjEN8gp4r7LCHqmIO5VzzvsT8QGRE3w4G47UwYDtAo,94
|
3
3
|
fastlife/adapters/jinjax/__init__.py,sha256=jy88zyqk7nFlaY-0lmgAoe0HyO5r_NKckQb3faQiUv4,137
|
4
|
-
fastlife/adapters/jinjax/renderer.py,sha256=
|
4
|
+
fastlife/adapters/jinjax/renderer.py,sha256=FOalTMnJ9kq_lqiiOnWq5N0x7RIYSlRZOWhux5F3RnU,14164
|
5
|
+
fastlife/adapters/jinjax/widget_factory/__init__.py,sha256=Dy_2xr_YDAyEF9WtNpjV-aYaehRO1iKEIHVFdfFeszw,59
|
6
|
+
fastlife/adapters/jinjax/widget_factory/base.py,sha256=cRVk2VqpQ7ZfrOslcJQD3eju3gGl2fACMWfcFyBPahs,1009
|
7
|
+
fastlife/adapters/jinjax/widget_factory/bool_builder.py,sha256=2-Hv5w4hfBfGWGetb00I8Lm1FDAputH2MNt3tCx-RbA,1280
|
8
|
+
fastlife/adapters/jinjax/widget_factory/emailstr_builder.py,sha256=GjRCT_kq9D6ZSu_Qs7ef2nj8gfaZMT0J-SpEj6NZWOg,1472
|
9
|
+
fastlife/adapters/jinjax/widget_factory/enum_builder.py,sha256=xMZpxik9zmpZbMTcldOHQRXYscNO-9YlcduY3GpFEQI,1516
|
10
|
+
fastlife/adapters/jinjax/widget_factory/factory.py,sha256=tD4RrOgWLqD1_R2ZVHiKDOdPCV5JDN3e6SgpklehBhQ,5649
|
11
|
+
fastlife/adapters/jinjax/widget_factory/literal_builder.py,sha256=mk8cmXDah_WRpy6wTRA6_du7UV6vxHoDb9ujgAAxH44,1677
|
12
|
+
fastlife/adapters/jinjax/widget_factory/model_builder.py,sha256=cqrb7zkJHy0r4angYRYnz5hyHx99EL4MbNl-sS6qq8I,2220
|
13
|
+
fastlife/adapters/jinjax/widget_factory/secretstr_builder.py,sha256=DrFXJeoajai7r1qfq8kBavdoo33-9DImmM4u8l_MKfQ,1562
|
14
|
+
fastlife/adapters/jinjax/widget_factory/sequence_builder.py,sha256=97aJ4K_pm1zDr_xNYUoO9UeziRT4VeFKIdkZ1gAcjdM,1928
|
15
|
+
fastlife/adapters/jinjax/widget_factory/set_builder.py,sha256=Qulao7i7pJNF1ZRzFdpJ-onQ2faW8IjACOi2sZyoYzA,2731
|
16
|
+
fastlife/adapters/jinjax/widget_factory/simpletype_builder.py,sha256=OFkbF_5_9DP56VQLXRXGi6_cm_6JmFgCzdCblnBb1aE,1670
|
17
|
+
fastlife/adapters/jinjax/widget_factory/union_builder.py,sha256=-FlqGejzfEiyKb8vgSaMbECr8libC4BprK6F2OA_12M,2825
|
5
18
|
fastlife/adapters/jinjax/widgets/__init__.py,sha256=HERnX9xiXUbTDz3XtlnHWABTBjhIq_kkBgWs5E6ZIMY,42
|
6
|
-
fastlife/adapters/jinjax/widgets/base.py,sha256=
|
19
|
+
fastlife/adapters/jinjax/widgets/base.py,sha256=3bBThRMnsdCi6Q_Dm73ep5pNOqgpSXsvAIBbHshfY7I,4037
|
7
20
|
fastlife/adapters/jinjax/widgets/boolean.py,sha256=w4hZMo_8xDoThStlIUR4eVfLm8JwUp0-TaGCjGSyCbA,1145
|
8
|
-
fastlife/adapters/jinjax/widgets/checklist.py,sha256=
|
9
|
-
fastlife/adapters/jinjax/widgets/dropdown.py,sha256=
|
10
|
-
fastlife/adapters/jinjax/widgets/
|
11
|
-
fastlife/adapters/jinjax/widgets/
|
12
|
-
fastlife/adapters/jinjax/widgets/
|
13
|
-
fastlife/adapters/jinjax/widgets/
|
14
|
-
fastlife/adapters/jinjax/widgets/
|
15
|
-
fastlife/adapters/jinjax/widgets/union.py,sha256=9u6AG9KzSQAT8ae9MmCniz0FJ8yGF12oRhwH5oWpWxE,2526
|
21
|
+
fastlife/adapters/jinjax/widgets/checklist.py,sha256=8fgOrdxy1xpyQ6p3_mbRMd2vx6EU2WT5jI7QF27Y5EQ,1664
|
22
|
+
fastlife/adapters/jinjax/widgets/dropdown.py,sha256=3Kc7i0z-7d6HrQchSHFCO5-xOh3bSEePo_pjXrIkvSE,1599
|
23
|
+
fastlife/adapters/jinjax/widgets/hidden.py,sha256=ZOJoUwMMgyabTFII38lnr8QRgVo370Go0VZ4qhEW1zc,720
|
24
|
+
fastlife/adapters/jinjax/widgets/model.py,sha256=t9A3C8wcptxvf7Mlrx9mUraxnG2p_39CrGnRq71t-A0,1322
|
25
|
+
fastlife/adapters/jinjax/widgets/sequence.py,sha256=60rgz4LgE_TQQwajiZhn6EhY-s-HXOiIdQiQoKlUCvQ,1533
|
26
|
+
fastlife/adapters/jinjax/widgets/text.py,sha256=KtUieF-q_BigG5AcL-4Sdr6LrIOQdWPwlaVW-2p-KPQ,3205
|
27
|
+
fastlife/adapters/jinjax/widgets/union.py,sha256=CO6Q4_U8DieVsS5NzMp6TAbVXrBljfcjSARycEKYPDY,2540
|
16
28
|
fastlife/components/A.jinja,sha256=rjnOFNpM2SUeJ0P8FDe3jzc_1WlsL65Kv-tb6KMCEpw,1389
|
17
29
|
fastlife/components/Button.jinja,sha256=COtCjDpzGLNqtBUYsHw7gdUay4kff3KdLJFAzrEnMmo,2310
|
18
30
|
fastlife/components/Checkbox.jinja,sha256=47_E9uPdr3QKUvRVhNQA7VE0uh5FVslQM26cdF0WCtY,753
|
19
31
|
fastlife/components/CsrfToken.jinja,sha256=ftqhcMibf1G8pbGCytlUcj5LGEmD8QJKwVKTro5w-ns,199
|
20
32
|
fastlife/components/Details.jinja,sha256=BKyhSU7bZdbd_deTjmAGcMbgUoQW3h8JSR3thH-2oJA,741
|
21
|
-
fastlife/components/Form.jinja,sha256=
|
33
|
+
fastlife/components/Form.jinja,sha256=Wb0nK5xuhqhkuQll9j76i3nBJcYCIjXG9RE9nWeoPZc,2095
|
22
34
|
fastlife/components/H1.jinja,sha256=ODwQMgwtuy2E2ShgamjFDlnCwOQQuuLIhvEzUF66nYM,375
|
23
35
|
fastlife/components/H2.jinja,sha256=LcBE2R_N50gio01nxH9qhp8_G1HxOT91xG_u8J8ae_Q,375
|
24
36
|
fastlife/components/H3.jinja,sha256=3PzxfQh07A35Og6dE5ow9BZdNp2Qnu7OuVeWREvD7Uo,375
|
@@ -1666,48 +1678,51 @@ fastlife/components/pydantic_form/Textarea.jinja,sha256=NzfCi5agRUSVcb5RXw0QamM8
|
|
1666
1678
|
fastlife/components/pydantic_form/Union.jinja,sha256=czTska54z9KCZKu-FaycLmOvtH6y6CGUFQ8DHnkjrJk,1461
|
1667
1679
|
fastlife/components/pydantic_form/Widget.jinja,sha256=EXskDqt22D5grpGVwlZA3ndve2Wr_6yQH4qVE9c31Og,397
|
1668
1680
|
fastlife/config/__init__.py,sha256=ThosRIPZ_fpD0exZu-kUC_f8ZNa5KyDlleWMmEHkjEo,448
|
1669
|
-
fastlife/config/configurator.py,sha256
|
1670
|
-
fastlife/config/exceptions.py,sha256=
|
1671
|
-
fastlife/config/openapiextra.py,sha256=
|
1681
|
+
fastlife/config/configurator.py,sha256=ooV2NJJB830GWRMMqkBIGMmqcGg0cEId2_HAqvqCLxg,22384
|
1682
|
+
fastlife/config/exceptions.py,sha256=kH2-akbzGeODlY_1bUhbzDKqBFrpOoqnVom0WPm0IGg,1237
|
1683
|
+
fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
|
1672
1684
|
fastlife/config/registry.py,sha256=dGcNm7E6WY0x5HZNzo1gBFvGFCWeJj6JFXsJtLax5NU,1347
|
1673
|
-
fastlife/config/resources.py,sha256=
|
1674
|
-
fastlife/config/settings.py,sha256=
|
1675
|
-
fastlife/config/views.py,sha256=
|
1685
|
+
fastlife/config/resources.py,sha256=Wu3vVr7XD18Gf4-MYYCxAAnuRmsAJmpllonts_BVGdQ,8593
|
1686
|
+
fastlife/config/settings.py,sha256=ecVczScdSJKOoXxE3ToQCcrK2AbHIXFVKKvf4jHd7TM,3902
|
1687
|
+
fastlife/config/views.py,sha256=V-P53GSnvqEPzkvEWNuI4ofcdbFur2Dl-s6BeKXObwI,2086
|
1676
1688
|
fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
|
1677
1689
|
fastlife/middlewares/base.py,sha256=9OYqByRuVoIrLt353NOedPQTLdr7LSmxhb2BZcp20qk,638
|
1678
1690
|
fastlife/middlewares/reverse_proxy/__init__.py,sha256=g1SoVDmenKzpAAPYHTEsWgdBByOxtLg9fGx6RV3i0ok,846
|
1679
|
-
fastlife/middlewares/reverse_proxy/x_forwarded.py,sha256=
|
1691
|
+
fastlife/middlewares/reverse_proxy/x_forwarded.py,sha256=0O9tziA63gQBmKATQz3B8H8G9CjZjnfM9NaisrvJHRY,1714
|
1680
1692
|
fastlife/middlewares/session/__init__.py,sha256=3XgXcIO6yQls5G7x8K2T8b7a_enA_7rQptWZcp3j2Ak,1400
|
1681
|
-
fastlife/middlewares/session/middleware.py,sha256=
|
1682
|
-
fastlife/middlewares/session/serializer.py,sha256=
|
1693
|
+
fastlife/middlewares/session/middleware.py,sha256=R48x3MJ-tu8siy8G12hDHa83sMcZz6E1eEb0xwk77E4,3166
|
1694
|
+
fastlife/middlewares/session/serializer.py,sha256=wpaktDP5v1spmbD-D3Q68EK9A0KInE4DT8mkogBJ3Fc,2157
|
1683
1695
|
fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1684
1696
|
fastlife/request/__init__.py,sha256=-wrh12uWM7ysmIUE6FszBit2H6iI5LWVEnHxQJ_23ZE,157
|
1685
|
-
fastlife/request/form.py,sha256=
|
1686
|
-
fastlife/request/form_data.py,sha256=
|
1697
|
+
fastlife/request/form.py,sha256=BiuvbV85NkvnJECSxqabfq3PhMfdKMPVobS35PhxSFA,3595
|
1698
|
+
fastlife/request/form_data.py,sha256=JZmKbKZz8nnspvCHYHQrz-xsFVJFaWHkTilUWk7Fx-M,4448
|
1687
1699
|
fastlife/request/localizer.py,sha256=9MXAcsod-Po5qeg4lttD3dyumiI0y5vGHCwSSmt9or8,349
|
1688
1700
|
fastlife/request/request.py,sha256=h4Ji0GElWJ3W2hHgOwjSKHQcxUjjPuOk09v-oyfjEd0,2568
|
1689
1701
|
fastlife/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
|
1690
|
-
fastlife/routing/route.py,sha256=
|
1691
|
-
fastlife/routing/router.py,sha256=
|
1702
|
+
fastlife/routing/route.py,sha256=vqjfMsHAVO0l2B8fuB8t19CKMtE7WoBkG4kvi4lUonM,1441
|
1703
|
+
fastlife/routing/router.py,sha256=ho9TvTkX2iUW6GEh99FgclZVFKkCCCxYG4pPHeUtGn8,482
|
1692
1704
|
fastlife/security/__init__.py,sha256=QYDcJ3oXQzqXQxoDD_6biGAtercFrtePttoifiL1j34,25
|
1693
|
-
fastlife/security/csrf.py,sha256=
|
1694
|
-
fastlife/security/policy.py,sha256=
|
1705
|
+
fastlife/security/csrf.py,sha256=PIKG83LPqKz4kDALnZxIyPdYVwbNqsIryi7JPqRPQag,2168
|
1706
|
+
fastlife/security/policy.py,sha256=ECNEyZXjizK2kz61v5eU7xFNd_M6tIlr9JEwcdyjuj8,5142
|
1695
1707
|
fastlife/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1696
|
-
fastlife/services/locale_negociator.py,sha256=
|
1697
|
-
fastlife/services/policy.py,sha256=
|
1698
|
-
fastlife/services/templates.py,sha256
|
1699
|
-
fastlife/services/translations.py,sha256=
|
1708
|
+
fastlife/services/locale_negociator.py,sha256=Np2O8s7xnYTpf5eCG7LvcfFJ2LV7p_k86NNrU9Lju88,846
|
1709
|
+
fastlife/services/policy.py,sha256=RfYGPjfEAAoHECUnZVLPZgN0iRanu8UKQSky6oAz81o,1687
|
1710
|
+
fastlife/services/templates.py,sha256=-dIt8zrgiRsjMblS174Rx_2xRZkQQRIATYhaA2vbIAk,3867
|
1711
|
+
fastlife/services/translations.py,sha256=Fu93zSc3ajNVFfAqw_G0nBV9bitss9Xy-he9lSHx0V8,4387
|
1700
1712
|
fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
|
1701
|
-
fastlife/shared_utils/infer.py,sha256=
|
1702
|
-
fastlife/shared_utils/resolver.py,sha256=
|
1713
|
+
fastlife/shared_utils/infer.py,sha256=3G_u6q2aWzeiVlAyGaWIlnAcz90m4bFNwpPYd5JIqfE,723
|
1714
|
+
fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB39E8,3713
|
1703
1715
|
fastlife/templates/__init__.py,sha256=QrP_5UAOgxqC-jOu5tcjd-l6GOYrS4dka6vmWMxWqfo,184
|
1704
|
-
fastlife/templates/binding.py,sha256=
|
1716
|
+
fastlife/templates/binding.py,sha256=0pE2btOwLf4xOEgBXVOyz_dIX9tBCYCaJ7RhZI3knbs,1464
|
1705
1717
|
fastlife/templates/constants.py,sha256=MGdUjkF9hsPMN8rOS49eWbAApcb8FL-FAeFvJU8k90M,8387
|
1706
|
-
fastlife/testing/__init__.py,sha256=
|
1707
|
-
fastlife/testing/
|
1718
|
+
fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
|
1719
|
+
fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
|
1720
|
+
fastlife/testing/form.py,sha256=ST0xNCoUqz_oD92cWHzQ6CbJ5hFopvu_NNKpOfiuYWY,7874
|
1721
|
+
fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
|
1722
|
+
fastlife/testing/testclient.py,sha256=WmUnGkDPuSd4dKzTiXWyHWlJ31zBbySvMH9m8p0acg8,6741
|
1708
1723
|
fastlife/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1709
|
-
fastlife/views/pydantic_form.py,sha256=
|
1710
|
-
fastlifeweb-0.
|
1711
|
-
fastlifeweb-0.
|
1712
|
-
fastlifeweb-0.
|
1713
|
-
fastlifeweb-0.
|
1724
|
+
fastlife/views/pydantic_form.py,sha256=4dv37JORLpvkgCgMGZfUN_qy7wme040GLZAzOTFqdnU,1367
|
1725
|
+
fastlifeweb-0.17.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
|
1726
|
+
fastlifeweb-0.17.0.dist-info/METADATA,sha256=xw0ntJeslB74GTpWv_9HlWRQgNlQ7HyAulG1FkKEQic,3480
|
1727
|
+
fastlifeweb-0.17.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
1728
|
+
fastlifeweb-0.17.0.dist-info/RECORD,,
|