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.
Files changed (56) hide show
  1. fastlife/adapters/jinjax/renderer.py +49 -25
  2. fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
  3. fastlife/adapters/jinjax/widget_factory/base.py +38 -0
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
  7. fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
  15. fastlife/adapters/jinjax/widgets/base.py +6 -4
  16. fastlife/adapters/jinjax/widgets/checklist.py +1 -1
  17. fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
  18. fastlife/adapters/jinjax/widgets/hidden.py +2 -0
  19. fastlife/adapters/jinjax/widgets/model.py +4 -1
  20. fastlife/adapters/jinjax/widgets/sequence.py +3 -2
  21. fastlife/adapters/jinjax/widgets/text.py +9 -10
  22. fastlife/adapters/jinjax/widgets/union.py +9 -7
  23. fastlife/components/Form.jinja +12 -0
  24. fastlife/config/configurator.py +23 -24
  25. fastlife/config/exceptions.py +4 -1
  26. fastlife/config/openapiextra.py +1 -0
  27. fastlife/config/resources.py +26 -27
  28. fastlife/config/settings.py +2 -0
  29. fastlife/config/views.py +3 -1
  30. fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
  31. fastlife/middlewares/session/middleware.py +2 -2
  32. fastlife/middlewares/session/serializer.py +6 -5
  33. fastlife/request/form.py +7 -6
  34. fastlife/request/form_data.py +2 -6
  35. fastlife/routing/route.py +3 -1
  36. fastlife/routing/router.py +1 -0
  37. fastlife/security/csrf.py +2 -1
  38. fastlife/security/policy.py +2 -1
  39. fastlife/services/locale_negociator.py +2 -1
  40. fastlife/services/policy.py +3 -2
  41. fastlife/services/templates.py +2 -1
  42. fastlife/services/translations.py +15 -8
  43. fastlife/shared_utils/infer.py +4 -3
  44. fastlife/shared_utils/resolver.py +64 -4
  45. fastlife/templates/binding.py +2 -1
  46. fastlife/testing/__init__.py +1 -0
  47. fastlife/testing/dom.py +140 -0
  48. fastlife/testing/form.py +204 -0
  49. fastlife/testing/session.py +67 -0
  50. fastlife/testing/testclient.py +7 -390
  51. fastlife/views/pydantic_form.py +4 -4
  52. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
  53. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
  54. fastlife/adapters/jinjax/widgets/factory.py +0 -525
  55. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
  56. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -1,10 +1,7 @@
1
1
  """Testing your application."""
2
2
 
3
- import re
4
- import time
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
 
@@ -1,4 +1,4 @@
1
- from typing import Optional, cast
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: Optional[str] = Query(None),
15
- name: Optional[str] = Query(None),
16
- token: Optional[str] = Query(None),
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.16.3
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
  [![Documentation](https://github.com/mardiros/fastlife/actions/workflows/gh-pages.yml/badge.svg)](https://mardiros.github.io/fastlife/)
38
38
  [![Continuous Integration](https://github.com/mardiros/fastlife/actions/workflows/main.yml/badge.svg)](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
39
39
  [![Coverage Report](https://codecov.io/gh/mardiros/fastlife/graph/badge.svg?token=DTpi73d7mf)](https://codecov.io/gh/mardiros/fastlife)
40
-
40
+ [![Maintainability](https://api.codeclimate.com/v1/badges/94d107797b15b5e8843e/maintainability)](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 [JinjaX](https://jinjax.scaletti.dev/) and an extensible [set of
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 [tailwindcss](https://tailwindcss.com/),
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=rj2KI4GB8tZstJwImdDYHuswxjdbGri2f8H6YuByaLo,13046
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=JoB1bLI97ZW2ycfBcHgrPxiOszE9t_SFFK5L1bR-cSo,4015
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=VIIfJ8JB7ZAISwFFiGZ7jfGuNJ9sjkhRSVFqDTr-RR4,1655
9
- fastlife/adapters/jinjax/widgets/dropdown.py,sha256=x2Y9BOfHfSuzWD_HNrvCkiJtKDxl8Vs05Uk8QKvRxyY,1622
10
- fastlife/adapters/jinjax/widgets/factory.py,sha256=XH-S1aucdPiujCWHdP_YyP1Tks7XzVKughMcL8eQhxk,17558
11
- fastlife/adapters/jinjax/widgets/hidden.py,sha256=pkMKxKhBKSGNf1Su81Jr-n8BJ45X5Qjsd1xXnJ7prPI,699
12
- fastlife/adapters/jinjax/widgets/model.py,sha256=XAvf125LAGmWOyIf7jMFzmGb6oRW_fb6Hg90tR_irTw,1272
13
- fastlife/adapters/jinjax/widgets/sequence.py,sha256=Xs3qic1pJLq-w7P7EXpa5taWnt5gWciK80gbhYG4rZM,1512
14
- fastlife/adapters/jinjax/widgets/text.py,sha256=fmbMwksgUpEImD8AExKCSp4g7LBD9F77dDU-2zWC8Qg,3260
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=B-l1c-Phe86Y0HfzJHvn-QFljHpsF0gEJcJ6PowuFEI,1520
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=-U-se932t95YiIfPD6N-CElWuuqbKP2AKTjJXoajCKE,22385
1670
- fastlife/config/exceptions.py,sha256=2MS2MFgb3mDbtdHEwnC-QYubob3Rl0v8O8y615LY0ds,1180
1671
- fastlife/config/openapiextra.py,sha256=_9rBYeTqB7nVuzvUHMwZU387bTfYFHYLlP05NP0vEDs,513
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=pM0j5VKVbVak4Z5mFRHBjAjUqORP4TAtCnZM3res5o8,8776
1674
- fastlife/config/settings.py,sha256=7oggPOucyJwQYI97q8vs3kPXjFIVpQu1q6BK25h-uFs,3789
1675
- fastlife/config/views.py,sha256=Dxi6lO4gFs6GriAW7Rh5GDvebwbrpS2HzYhf30pXJiE,2058
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=aq_80V0Vrb4M6RQfk8CtQnhfwZGzJyeTYYQ16In8DaQ,1510
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=AlRIFXfn3JesKJzMAFUHDOo22mfuwDHkyecDHo9jCdA,3172
1682
- fastlife/middlewares/session/serializer.py,sha256=fTdZCop0y4VkCMyOIo6GEbo5fVWrwsBXaSWfConPL8E,2144
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=Ln9TySvAccYhFY9zc49P6YGE9cQSi_o3mByxvcuCIKY,3516
1686
- fastlife/request/form_data.py,sha256=DNKpXUxeDMYb43zSBnSPTBjAB8OhsYlNjERSsLgFdvI,4454
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=O0gwPtP7ur2EHRf76kBASgoLMQciGHXcrJkW8zEPFJA,1413
1691
- fastlife/routing/router.py,sha256=bLZ4k2aDs4L423znwGnw-X2LnM36O8fjhDWc8q1WewI,481
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=PvC9Fqdb6c0IzzsnaMx2quQdjjKrb-nOPoAHfcwoAe8,2141
1694
- fastlife/security/policy.py,sha256=8FWtjcOjkK5UvnjKluVkJIZdec2bfXbkKx7ouiiyjBs,5115
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=b1Fsx-r_zIZe0GD8WH1gmslMb0mNrvxuacxD4LNX63o,819
1697
- fastlife/services/policy.py,sha256=ZK4K3fVGT1fUeBdo5sSWnEcJg49CZuaI3z2gyg3KguQ,1653
1698
- fastlife/services/templates.py,sha256=7gPJxGWD-XqputbZpy_Icsz3WHKJaWg2JgkVOeKrjfA,3840
1699
- fastlife/services/translations.py,sha256=Bo5CIjdbQ3g_ihbv4Bz60hzd8VOtqEEPOyhJEbGcvP4,4363
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=CJjsL_F5OQRG7-0_89MQiZhyd7IcMGyirlQhjtcaIT4,728
1702
- fastlife/shared_utils/resolver.py,sha256=BRU88Ej4oA1iDIyG4Z6T7Q9WFvPHiMm6zuSh623312A,1725
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=00rAbPHttCOgY-Nl9TbwiM2iFtSoH7xcBlj9kbNLzrE,1437
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=vuqwoNUd3BuIp3fm7nkvmYkIGjIimf5zUGhDkeWrg2s,98
1707
- fastlife/testing/testclient.py,sha256=BC7lLQ_jc59UmknAKzgRtW9a3cpX_V_QLp9Mg2ScLA8,20546
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=ZYOXKudmSqtRvFn5ZY75DOXZVunGXJBKpjh9FJcqu6k,1386
1710
- fastlifeweb-0.16.3.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1711
- fastlifeweb-0.16.3.dist-info/METADATA,sha256=oKBtNXGfKC8ya3PoQYclQuJVP_UPZs4GzKmWUzt3_t8,3345
1712
- fastlifeweb-0.16.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1713
- fastlifeweb-0.16.3.dist-info/RECORD,,
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,,