fastlifeweb 0.26.1__py3-none-any.whl → 0.27.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.
CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.27.0 - Released on 2025-04-24
2
+ * Add i18n support on pydantic form.
3
+
4
+ ## 0.26.2 - Released on 2025-04-20
5
+ * Add a RedirectResponse class that is htmx friendly and P/R/G pattern friendly.
6
+ * Fix documentation generation.
7
+
1
8
  ## 0.26.1 - Released on 2025-04-20
2
9
  * Add new helpers for the webtestclient element.
3
10
 
fastlife/__init__.py CHANGED
@@ -2,8 +2,6 @@ from importlib import metadata
2
2
 
3
3
  __version__ = metadata.version("fastlifeweb")
4
4
 
5
- from fastapi import Response
6
- from fastapi.responses import RedirectResponse
7
5
 
8
6
  from .adapters.fastapi.form import form_model
9
7
  from .adapters.fastapi.localizer import Localizer
@@ -26,6 +24,7 @@ from .config import (
26
24
  from .domain.model.asgi import ASGIRequest, ASGIResponse
27
25
  from .domain.model.form import FormModel
28
26
  from .domain.model.request import GenericRequest
27
+ from .domain.model.response import RedirectResponse, Response
29
28
  from .domain.model.security_policy import (
30
29
  Allowed,
31
30
  Anonymous,
@@ -55,7 +55,7 @@ class Widget(JinjaXTemplate, Generic[T]):
55
55
  aria_label: str | None = Field(default=None)
56
56
  "Non visible text alternative."
57
57
  token: str = Field(default="")
58
- "unique token to ensure id are unique in the DOM."
58
+ "Unique token to ensure id are unique in the DOM."
59
59
  removable: bool = Field(default=False)
60
60
  "Indicate that the widget is removable from the dom."
61
61
 
@@ -16,7 +16,7 @@ class BooleanWidget(Widget[bool]):
16
16
  <div class="flex items-center">
17
17
  <Checkbox :name="name" :id="id" :checked="value" value="1" />
18
18
  <Label :for="id" class="ms-2 text-base text-neutral-900 dark:text-white">
19
- {{title|safe}}
19
+ {{ gettext(title)|safe }}
20
20
  </Label>
21
21
  </div>
22
22
  <pydantic_form.Error :text="error" />
@@ -40,7 +40,7 @@ class ChecklistWidget(Widget[Sequence[Checkable]]):
40
40
  <div class="pt-4">
41
41
  <Details>
42
42
  <Summary :id="id + '-summary'">
43
- <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
43
+ <H3 :class="H3_SUMMARY_CLASS">{{ gettext(title) }}</H3>
44
44
  <pydantic_form.Error :text="error" />
45
45
  </Summary>
46
46
  <div>
@@ -52,7 +52,7 @@ class ChecklistWidget(Widget[Sequence[Checkable]]):
52
52
  :checked="value.checked" />
53
53
  <Label :for="value.id"
54
54
  class="ms-2 text-base text-neutral-900 dark:text-white">
55
- {{- value.label -}}
55
+ {{- gettext(value.label) -}}
56
56
  </Label>
57
57
  <pydantic_form.Error :text="value.error" />
58
58
  </div>
@@ -17,12 +17,12 @@ class DropDownWidget(Widget[str]):
17
17
  template = """
18
18
  <pydantic_form.Widget :widget_id="id" :removable="removable">
19
19
  <div class="pt-4">
20
- <Label :for="id">{{title}}</Label>
20
+ <Label :for="id">{{ gettext(title) }}</Label>
21
21
  <Select :name="name" :id="id">
22
22
  {%- for opt in options -%}
23
23
  <Option :value="opt.value" id={{id + "-" + opt.value.replace(" ", " -")}}
24
24
  :selected="value==opt.value">
25
- {{- opt.text -}}
25
+ {{- gettext(opt.text) -}}
26
26
  </Option>
27
27
  {%- endfor -%}
28
28
  </Select>
@@ -11,7 +11,7 @@ class MFACodeWidget(Widget[str]):
11
11
  template = """
12
12
  <pydantic_form.Widget :widget_id="id" :removable="removable">
13
13
  <div class="pt-4">
14
- <Label :for="id">{{title}}</Label>
14
+ <Label :for="id">{{ gettext(title) }}</Label>
15
15
  <pydantic_form.Error :text="error" />
16
16
  <Input :name="name" type="text" :id="id" inputmode="numeric"
17
17
  autocomplete="one-time-code" :autofocus="autofocus"
@@ -17,7 +17,7 @@ class ModelWidget(Widget[Sequence[TWidget]]):
17
17
  {% if nested %}
18
18
  <Details>
19
19
  <Summary :id="id + '-summary'">
20
- <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
20
+ <H3 :class="H3_SUMMARY_CLASS">{{ gettext(title) }}</H3>
21
21
  <pydantic_form.Error :text="error" />
22
22
  </Summary>
23
23
  <div>
@@ -14,7 +14,7 @@ class SequenceWidget(Widget[Sequence[TWidget]]):
14
14
  <pydantic_form.Widget :widget_id="id" :removable="removable">
15
15
  <Details :id="id">
16
16
  <Summary :id="id + '-summary'">
17
- <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
17
+ <H3 :class="H3_SUMMARY_CLASS">{{ gettext(title) }}</H3>
18
18
  <pydantic_form.Error :text="error" />
19
19
  </Summary>
20
20
  <div>
@@ -15,7 +15,7 @@ class TextWidget(Widget[Builtins]):
15
15
  template = """
16
16
  <pydantic_form.Widget :widget_id="id" :removable="removable">
17
17
  <div class="pt-4">
18
- <Label :for="id">{{title}}</Label>
18
+ <Label :for="id">{{ gettext(title) }}</Label>
19
19
  <pydantic_form.Error :text="error" />
20
20
  <Input :name="name" :value="value" :type="input_type" :id="id"
21
21
  :aria-label="aria_label" :placeholder="placeholder"
@@ -26,19 +26,22 @@ class TextWidget(Widget[Builtins]):
26
26
  """
27
27
 
28
28
  input_type: str = Field(default="text")
29
+ """type attribute for the Input component."""
29
30
  placeholder: str | None = Field(default=None)
31
+ """placeholder attribute for the Input component."""
30
32
  autocomplete: str | None = Field(default=None)
33
+ """autocomplete attribute for the Input component."""
31
34
 
32
35
 
33
36
  class PasswordWidget(Widget[SecretStr]):
34
37
  """
35
- Widget for text like field (email, ...).
38
+ Widget for password fields.
36
39
  """
37
40
 
38
41
  template = """
39
42
  <pydantic_form.Widget :widget_id="id" :removable="removable">
40
43
  <div class="pt-4">
41
- <Label :for="id">{{title}}</Label>
44
+ <Label :for="id">{{ gettext(title) }}</Label>
42
45
  <pydantic_form.Error :text="error" />
43
46
  <Password :name="name" :type="input_type" :id="id"
44
47
  autocomplete={{
@@ -51,8 +54,13 @@ class PasswordWidget(Widget[SecretStr]):
51
54
  """
52
55
 
53
56
  input_type: str = Field(default="password")
57
+ """type attribute for the Input component."""
54
58
  placeholder: str | None = Field(default=None)
59
+ """placeholder attribute for the Input component."""
55
60
  new_password: bool = Field(default=False)
61
+ """
62
+ Adapt autocomplete behavior for browsers to hint existing or generate password.
63
+ """
56
64
 
57
65
 
58
66
  class TextareaWidget(Widget[str | Sequence[str]]):
@@ -81,7 +89,7 @@ class TextareaWidget(Widget[str | Sequence[str]]):
81
89
  template = """
82
90
  <pydantic_form.Widget :widget_id="id" :removable="removable">
83
91
  <div class="pt-4">
84
- <Label :for="id">{{title}}</Label>
92
+ <Label :for="id">{{ gettext(title) }}</Label>
85
93
  <pydantic_form.Error :text="error" />
86
94
  <Textarea :name="name" :id="id" :aria-label="aria_label">
87
95
  {%- if value is string -%}
@@ -94,5 +102,3 @@ class TextareaWidget(Widget[str | Sequence[str]]):
94
102
  </div>
95
103
  </pydantic_form.Widget>
96
104
  """
97
-
98
- placeholder: str = Field(default="")
@@ -23,7 +23,7 @@ class UnionWidget(Widget[TWidget]):
23
23
  <div id="{{id}}">
24
24
  <Details>
25
25
  <Summary :id="id + '-union-summary'">
26
- <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
26
+ <H3 :class="H3_SUMMARY_CLASS">{{ gettext(title) }}</H3>
27
27
  <pydantic_form.Error :text="error" />
28
28
  </Summary>
29
29
  <div hx-sync="this" id="{{id}}-child">
@@ -37,7 +37,7 @@ class UnionWidget(Widget[TWidget]):
37
37
  :hx-vals="typ.params|tojson"
38
38
  :id="typ.id"
39
39
  onclick={{ "document.getElementById('" + id + "-remove-btn').hidden=false" }}
40
- :class="SECONDARY_BUTTON_CLASS">{{typ.title}}</Button>
40
+ :class="SECONDARY_BUTTON_CLASS">{{ gettext(typ.title) }}</Button>
41
41
  {% endfor %}
42
42
  {% endif %}
43
43
  </div>
@@ -6,7 +6,7 @@
6
6
 
7
7
  ::
8
8
 
9
- <Form :hx-post="true">
9
+ <Form hx-post>
10
10
  <Input name="name" placeholder="Bob" />
11
11
  <Button>Submit</Button>
12
12
  </Form>
@@ -42,8 +42,8 @@
42
42
 
43
43
  <input name="{{name}}" value="{{value|default('')}}" type="{{type}}"
44
44
  {%- if id %} id="{{id}}" {%- endif %}
45
- {%- if aria_label %} aria-label="{{aria_label}}" {%- endif %}
46
- {%- if placeholder %} placeholder="{{placeholder}}" {%- endif %}
45
+ {%- if aria_label %} aria-label="{{ gettext(aria_label) }}" {%- endif %}
46
+ {%- if placeholder %} placeholder="{{ gettext(placeholder) }}" {%- endif %}
47
47
  {%- if inputmode %} inputmode="{{inputmode}}" {%- endif %}
48
48
  {%- if autocomplete %} autocomplete="{{autocomplete}}" {%- endif %}
49
49
  class="{{attrs.class or INPUT_CLASS}}"
@@ -14,5 +14,5 @@
14
14
  {%- if attrs.for %} for="{{attrs.for}}" {%- endif %}
15
15
  class="{{attrs.class or LABEL_CLASS}}"
16
16
  {%- if id %} id="{{id}}" {%- endif %}>
17
- {{-content -}}
17
+ {{- content -}}
18
18
  </label>
@@ -493,14 +493,7 @@ class GenericConfigurator(Generic[TRegistry]):
493
493
  custom_globals[key] = val
494
494
  return {
495
495
  "request": request,
496
- "gettext": lczr.gettext,
497
- "ngettext": lczr.ngettext,
498
- "dgettext": lczr.dgettext,
499
- "dngettext": lczr.dngettext,
500
- "pgettext": lczr.pgettext,
501
- "dpgettext": lczr.dpgettext,
502
- "npgettext": lczr.npgettext,
503
- "dnpgettext": lczr.dnpgettext,
496
+ **lczr.as_dict(),
504
497
  **custom_globals,
505
498
  **request.renderer_globals,
506
499
  }
@@ -69,3 +69,13 @@ class GenericRequest(ASGIRequest, Generic[TRegistry, TIdentity, TClaimedIdentity
69
69
  )
70
70
 
71
71
  return await self.security_policy.has_permission(permission)
72
+
73
+ def url_path_for(self, name: str, /, **path_params: Any) -> str:
74
+ """
75
+ Return the url pathinfo for the given route and route parameters.
76
+
77
+ :param name: the name of the route
78
+ :param path_params: parameters for the route.
79
+ """
80
+ url_path_provider: Any = self.scope.get("router") or self.scope.get("app")
81
+ return url_path_provider.url_path_for(name, **path_params)
@@ -0,0 +1,40 @@
1
+ from collections.abc import Mapping
2
+ from urllib.parse import quote
3
+
4
+ from starlette.background import BackgroundTask
5
+ from starlette.datastructures import URL
6
+ from starlette.responses import Response
7
+
8
+
9
+ class RedirectResponse(Response):
10
+ """
11
+ A redirect response for Post/Redirect/Get pattern.
12
+
13
+ The starlette default value status code is 307, which means that it is used
14
+ as a way to replay the same query which is definitly not the most used case
15
+ in web applications.
16
+
17
+ This is why the redirect response here is using 303 see other which
18
+ ensure a GET request will be made for the redirection.
19
+
20
+ A new parameter hx_redirect exists in order to set the HX-Redirect header
21
+ to follow a browser redirection from an ajax query.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ url: str | URL,
27
+ hx_redirect: bool = False,
28
+ status_code: int = 303,
29
+ headers: Mapping[str, str] | None = None,
30
+ background: BackgroundTask | None = None,
31
+ ):
32
+ super().__init__(
33
+ content=b"", status_code=status_code, headers=headers, background=background
34
+ )
35
+ self.headers["HX-Redirect" if hx_redirect else "location"] = quote(
36
+ str(url), safe=":/%#?=@[]!$&'()*+,;"
37
+ )
38
+
39
+
40
+ __all__ = ["Response", "RedirectResponse"]
@@ -2,9 +2,10 @@
2
2
 
3
3
  import pathlib
4
4
  from collections import defaultdict
5
- from collections.abc import Callable, Iterator
5
+ from collections.abc import Callable, Iterator, Mapping
6
6
  from gettext import GNUTranslations
7
7
  from io import BufferedReader
8
+ from typing import Any
8
9
 
9
10
  from fastlife.shared_utils.resolver import resolve_path
10
11
 
@@ -100,10 +101,23 @@ class Localizer:
100
101
  self.translations[domain].merge(trans)
101
102
  self.global_translations.merge(trans)
102
103
 
103
- def __call__(self, message: str, mapping: dict[str, str] | None = None) -> str:
104
- return self.gettext(message, mapping)
105
-
106
- def gettext(self, message: str, mapping: dict[str, str] | None = None) -> str:
104
+ def as_dict(self) -> Mapping[str, Callable[..., str]]:
105
+ return {
106
+ "_": self.gettext,
107
+ "gettext": self.gettext,
108
+ "ngettext": self.ngettext,
109
+ "dgettext": self.dgettext,
110
+ "dngettext": self.dngettext,
111
+ "pgettext": self.pgettext,
112
+ "dpgettext": self.dpgettext,
113
+ "npgettext": self.npgettext,
114
+ "dnpgettext": self.dnpgettext,
115
+ }
116
+
117
+ def __call__(self, message: str, /, **mapping: Any) -> str:
118
+ return self.gettext(message, **mapping)
119
+
120
+ def gettext(self, message: str, /, **mapping: Any) -> str:
107
121
  if isinstance(message, TranslatableString):
108
122
  ret = self.translations[message.domain].gettext(message) # type: ignore
109
123
  else:
@@ -112,16 +126,12 @@ class Localizer:
112
126
  ret = ret.format(**mapping)
113
127
  return ret
114
128
 
115
- def ngettext(
116
- self, singular: str, plural: str, n: int, mapping: dict[str, str] | None = None
117
- ) -> str:
129
+ def ngettext(self, singular: str, plural: str, n: int, /, **mapping: Any) -> str:
118
130
  ret = self.global_translations.ngettext(singular, plural, n)
119
- mapping_num = {"num": n, **(mapping or {})}
131
+ mapping_num = {"num": n, **mapping}
120
132
  return ret.format(**mapping_num)
121
133
 
122
- def dgettext(
123
- self, domain: str, message: str, mapping: dict[str, str] | None = None
124
- ) -> str:
134
+ def dgettext(self, domain: str, message: str, /, **mapping: Any) -> str:
125
135
  ret = self.translations[domain].gettext(message)
126
136
  if mapping:
127
137
  ret = ret.format(**mapping)
@@ -133,15 +143,14 @@ class Localizer:
133
143
  singular: str,
134
144
  plural: str,
135
145
  n: int,
136
- mapping: dict[str, str] | None = None,
146
+ /,
147
+ **mapping: Any,
137
148
  ) -> str:
138
149
  ret = self.translations[domain].ngettext(singular, plural, n)
139
- mapping_num = {"num": n, **(mapping or {})}
150
+ mapping_num = {"num": n, **mapping}
140
151
  return ret.format(**mapping_num)
141
152
 
142
- def pgettext(
143
- self, context: str, message: str, mapping: dict[str, str] | None = None
144
- ) -> str:
153
+ def pgettext(self, context: str, message: str, /, **mapping: Any) -> str:
145
154
  ret = self.global_translations.pgettext(context, message)
146
155
  if mapping:
147
156
  ret = ret.format(**mapping)
@@ -152,7 +161,8 @@ class Localizer:
152
161
  domain: str,
153
162
  context: str,
154
163
  message: str,
155
- mapping: dict[str, str] | None = None,
164
+ /,
165
+ **mapping: Any,
156
166
  ) -> str:
157
167
  ret = self.translations[domain].pgettext(context, message)
158
168
  if mapping:
@@ -165,10 +175,11 @@ class Localizer:
165
175
  singular: str,
166
176
  plural: str,
167
177
  n: int,
168
- mapping: dict[str, str] | None = None,
178
+ /,
179
+ **mapping: Any,
169
180
  ) -> str:
170
181
  ret = self.global_translations.npgettext(context, singular, plural, n)
171
- mapping_num = {"num": n, **(mapping or {})}
182
+ mapping_num = {"num": n, **mapping}
172
183
  return ret.format(**mapping_num)
173
184
 
174
185
  def dnpgettext(
@@ -178,10 +189,11 @@ class Localizer:
178
189
  singular: str,
179
190
  plural: str,
180
191
  n: int,
181
- mapping: dict[str, str] | None = None,
192
+ /,
193
+ **mapping: Any,
182
194
  ) -> str:
183
195
  ret = self.translations[domain].npgettext(context, singular, plural, n)
184
- mapping_num = {"num": n, **(mapping or {})}
196
+ mapping_num = {"num": n, **mapping}
185
197
  return ret.format(**mapping_num)
186
198
 
187
199
 
@@ -158,9 +158,14 @@ class WebTestClient:
158
158
  method = "GET"
159
159
  headers = None
160
160
  content = None
161
+ url = resp.headers.get("location")
162
+ if "HX-Redirect" in resp.headers:
163
+ # Redirection requested to the browser from an AJAX request.
164
+ url = resp.headers["HX-Redirect"]
165
+ assert url, "Redirect response without a redirection"
161
166
  return self.request(
162
167
  method=method,
163
- url=resp.headers["location"],
168
+ url=url,
164
169
  content=content,
165
170
  headers=headers,
166
171
  max_redirects=max_redirects - 1,
@@ -31,6 +31,11 @@ async def show_widget(
31
31
  field = FieldInfo(title=title)
32
32
  # FIXME: .jinja should not be hardcoded
33
33
  renderer = cast(JinjaxRenderer, request.registry.get_renderer(".jinja")(request))
34
+ lczr = request.registry.localizer(request.locale_name)
35
+ renderer.globals = {
36
+ "request": request,
37
+ **lczr.as_dict(),
38
+ }
34
39
  data = renderer.pydantic_form_field(
35
40
  model=model_cls, # type: ignore
36
41
  name=name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.26.1
3
+ Version: 0.27.0
4
4
  Summary: High-level web framework
5
5
  Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
- CHANGELOG.md,sha256=T96sQ3Cf4MXmutaDj0-js6kwr0hVzDJA9krCjrV557g,8680
2
- fastlife/__init__.py,sha256=IZ9VQvlBduZ3WjfDk05rZNdCKNxWWqPaMvG0ZWtMTpU,2489
1
+ CHANGELOG.md,sha256=SaMUOfsgJvjHaEzgvgmtggz0FY0YEqltXogCMACKrvc,8904
2
+ fastlife/__init__.py,sha256=Fe8JiQyKIN1WGagUGFct-QBW8-Ku5vXhc_7BkFUGcWk,2475
3
3
  fastlife/adapters/__init__.py,sha256=imPD1hImpgrYkvUJRhHA5kVyGAua7VbP2WGkhSWKJT8,93
4
4
  fastlife/adapters/fastapi/__init__.py,sha256=1goV1FGFP04TGyskJBLKZam4Gvt1yoAvLMNs4ekWSSQ,243
5
5
  fastlife/adapters/fastapi/form.py,sha256=csxsDI6RK-g41pMwFhaVQCLDhF7dAZzgUp-VcrC3NFY,823
@@ -32,16 +32,16 @@ fastlife/adapters/jinjax/widget_factory/set_builder.py,sha256=kwtVLATkoOFTcKBKHk
32
32
  fastlife/adapters/jinjax/widget_factory/simpletype_builder.py,sha256=olP66B9AMY1X8fgEAxhMdozWN_w1TtcIAIW6uPJRSng,1570
33
33
  fastlife/adapters/jinjax/widget_factory/union_builder.py,sha256=qQOK3Y4I0Tg0XOzU_DwseaaKRmqQ7ORMfFyIHd6oysU,2841
34
34
  fastlife/adapters/jinjax/widgets/__init__.py,sha256=HERnX9xiXUbTDz3XtlnHWABTBjhIq_kkBgWs5E6ZIMY,42
35
- fastlife/adapters/jinjax/widgets/base.py,sha256=jU-oklWuM6MPlNzBvYO0HtxUrUteb-Dh96OudPLFJx4,4120
36
- fastlife/adapters/jinjax/widgets/boolean.py,sha256=GFvyO2Lz-vTJ8lpX2UrkJk7ymu0vU-xnBBEmyBKamJg,613
37
- fastlife/adapters/jinjax/widgets/checklist.py,sha256=xsr_edMu1nTcNnMEs1AAS3U-ugK95pEqn5NV5hjTeI0,1747
38
- fastlife/adapters/jinjax/widgets/dropdown.py,sha256=ou8U48txShr8IXdwF4_pBvTn1VcR9SazZKzbLcsN3_0,1458
35
+ fastlife/adapters/jinjax/widgets/base.py,sha256=M1Mu4ArU_YCxRrhisaMMzjKMNCw7npX5DRfdZyVKuks,4120
36
+ fastlife/adapters/jinjax/widgets/boolean.py,sha256=gHErI8tp_6J3kuqjL4-i1dw-UuR65zDsOCcVKcg_s4E,624
37
+ fastlife/adapters/jinjax/widgets/checklist.py,sha256=lbt2HL8-ie8FxHEXIx9etxfnnDWi-hfM6NXddLlS1fY,1767
38
+ fastlife/adapters/jinjax/widgets/dropdown.py,sha256=zcm1M7EQLISWr4sbZDinvUR_mgmdezrLHdC7wA0dQsA,1478
39
39
  fastlife/adapters/jinjax/widgets/hidden.py,sha256=IKcVYs6NjN8YjW-UTr3DRBong6Wrc0QLgcp8U9JoQmE,638
40
- fastlife/adapters/jinjax/widgets/mfa_code.py,sha256=8HHaEHyUEQT6xzJ0Y4O0eA1SU3wSfzXljx8XY73o-Rc,733
41
- fastlife/adapters/jinjax/widgets/model.py,sha256=YBIEWa_6mnmrBnesXjLTrpJ4drUS2CIorNmhK24cz7Q,1298
42
- fastlife/adapters/jinjax/widgets/sequence.py,sha256=dVoHQmHloaRuU1Sd82b2jnO8WDfdwM2FaZlLCJCps1o,2566
43
- fastlife/adapters/jinjax/widgets/text.py,sha256=TfmlJU233aZWIl-4cmm-f-pFxp6ycHWHnbiluOvRDgM,3040
44
- fastlife/adapters/jinjax/widgets/union.py,sha256=roCoFA82dLjF1XFW6UYaV7SCQWdFsSAT8Ux7KEB6_Us,2602
40
+ fastlife/adapters/jinjax/widgets/mfa_code.py,sha256=dI0CKlx2jCwujfnud_uIjnBCHvlG_gtapL1Iuan5wVg,744
41
+ fastlife/adapters/jinjax/widgets/model.py,sha256=STojkUMfCP1kyPFzzWS-MIZOoxAEFHTIW_jxFnODPb4,1309
42
+ fastlife/adapters/jinjax/widgets/sequence.py,sha256=aL93-ytj6nlbT8vumt3br6Jq8D97iiCXU3eQXrGYuG0,2577
43
+ fastlife/adapters/jinjax/widgets/text.py,sha256=XKpoLoBNsvQhHrezKCEcXlF9iQzuclXGaeFu6uq9_5A,3390
44
+ fastlife/adapters/jinjax/widgets/union.py,sha256=fNWmAXNEPkDbP14q6HmlD65L7q5_JmspZ_IG_SqB_NM,2624
45
45
  fastlife/assets/dist.css,sha256=d2ez-igscOaeYtJQc2FQuXlN17cYX13sUansFdg_kdA,19753
46
46
  fastlife/assets/source.css,sha256=0KtDcsKHj9LOcqNR1iv9pACwNBaNWkieEDqqjkgNL_s,47
47
47
  fastlife/components/A.jinja,sha256=MDNJ2auIeYbpNeErvJdlGid4nIKfbi85ArmMgChsCJU,1384
@@ -49,7 +49,7 @@ fastlife/components/Button.jinja,sha256=itKU-ct45XissU33yfmTekyHsNe00fr4RQL-e9cx
49
49
  fastlife/components/Checkbox.jinja,sha256=g62A1LR8TaN60h94pE2e5l9_eMmgnhVVE9HVCQtVVMo,748
50
50
  fastlife/components/CsrfToken.jinja,sha256=mS0q-3_hAevl_waWEPaN0QAYOBzMyzl-W1PSpEHUBA0,215
51
51
  fastlife/components/Details.jinja,sha256=NtQX-V3kcp1CV1GkrMkj5fc-KHPZHshWkrhXAZ8E3ms,736
52
- fastlife/components/Form.jinja,sha256=IJhkUwp_dF-BePi8okh-sUglXP6wJvtxNd-4PUFCwTY,2089
52
+ fastlife/components/Form.jinja,sha256=p4HNIpjDLJT0oH_Vbv6_lYV0tuG_2Xu5E2vZ-yJWrT8,2081
53
53
  fastlife/components/H1.jinja,sha256=fWZtTq34qN9gwGgDF91lkypxaXvGN_OTmUY7XUHIpw0,370
54
54
  fastlife/components/H2.jinja,sha256=o7Q-oR_zDtItV5A7QWfEo_LoMw6bR44YNBDQP3ao1bg,370
55
55
  fastlife/components/H3.jinja,sha256=cZHJTVER1LVYWAwP3sR-23Ktbk9WYlLgLnr2Dz_0oqU,370
@@ -57,8 +57,8 @@ fastlife/components/H4.jinja,sha256=w-n0bFqR_38oIuju_Bs_8OwYCtLP0gIfSsoi5u3U8GM,
57
57
  fastlife/components/H5.jinja,sha256=bpphjO54yrKLKt64voR5wCvxwFpxQRfkZg_OqhCPAcA,370
58
58
  fastlife/components/H6.jinja,sha256=9qzd6LpaLk5oTLdKw3utSVyX-uq7fn837mm22zG23Ko,370
59
59
  fastlife/components/Hidden.jinja,sha256=-D74wZ7qp2n5l_8HKmDhX5v_M2sAJ5l-w_z9m5d5KvA,283
60
- fastlife/components/Input.jinja,sha256=bg2YCVGQsXwNF5zPauNg0VYbhf3qzEW12t6Xz7bWYVI,1950
61
- fastlife/components/Label.jinja,sha256=t50MUGyNSFB4LK-J0D0fM2xMT_vCsG3fnO0OapvzPJQ,532
60
+ fastlife/components/Input.jinja,sha256=Orp_gTo40KfICLZECc8oLWnaBFixYzmzLRVzoAUe8hc,1972
61
+ fastlife/components/Label.jinja,sha256=5cYezFHNh5Nuytxbgo60fCeCeiiGS-Cs_fVToj7znx0,533
62
62
  fastlife/components/Option.jinja,sha256=x6t7uUQsI1YSRstD4bSVlgK2wm8NJUXnzOWfNWGlk_Y,448
63
63
  fastlife/components/P.jinja,sha256=Jumlwu9Wix8E2K7QwwimgWTrMdrFDAEfdLHlkz_Mp-g,371
64
64
  fastlife/components/Password.jinja,sha256=dSjPzzgBJM1K1hg_9UURPLpvUcwnna8hf6lH0nsYEps,1903
@@ -1690,7 +1690,7 @@ fastlife/components/pydantic_form/FatalError.jinja,sha256=ADtQvmo-e-NmDcFM1E6wZV
1690
1690
  fastlife/components/pydantic_form/Hint.jinja,sha256=8leBpfMGDmalc_KAjr2paTojr_rwq-luS6m_1BGj7Tw,202
1691
1691
  fastlife/components/pydantic_form/Widget.jinja,sha256=PgguUpvhG6CY9AW6H8qQMjKqjlybjDCAaFFAOHzrzVQ,418
1692
1692
  fastlife/config/__init__.py,sha256=5qpuaVYqi-AS0GgsfggM6rFsSwXgrqrLBo9jH6dVroc,407
1693
- fastlife/config/configurator.py,sha256=2LkPXoarMTk40S7AVlxODPAckDllE0G8u27jtPmvGM8,25188
1693
+ fastlife/config/configurator.py,sha256=82YEVRoI6VoyFPIbkWgnqlLIVNSVP9TUkbKl6qksWs8,24898
1694
1694
  fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
1695
1695
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1696
1696
  fastlife/config/resources.py,sha256=EcPTM25pnHcGFTtXjeZnWn5Mo_-8rhJ72HJ6rxnjPg8,8389
@@ -1700,7 +1700,8 @@ fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wN
1700
1700
  fastlife/domain/model/asgi.py,sha256=Cz45TZOtrh2pBVZr37aJ9jpnJH9BeNHrsvk9bq1nBc0,526
1701
1701
  fastlife/domain/model/csrf.py,sha256=BUiWK-S7rVciWHO1qTkM8e_KxzpF6gGC4MMJK1v6iDo,414
1702
1702
  fastlife/domain/model/form.py,sha256=JP6uumlZBYhiPxzcdxOsfsFm5BRfvkDFvlUCD6Vy8dI,3275
1703
- fastlife/domain/model/request.py,sha256=HgUSnUu3q18e07y57PadN3pPQwYrIZS1YEhYkBZ_Zfg,2674
1703
+ fastlife/domain/model/request.py,sha256=hHtGsfVND3TSG7HQZI_I0n4Gp4KyaAtsjaZEc4lwrv0,3090
1704
+ fastlife/domain/model/response.py,sha256=Vsd2zYGGhH0D2DlfiKz1CX9OJZ_ZYoEv_-foMZpDFZo,1294
1704
1705
  fastlife/domain/model/security_policy.py,sha256=f9SLi54vvRU-KSPJ5K0unoqYpkxIyzuZjKf2Ylwf5Rg,4796
1705
1706
  fastlife/domain/model/template.py,sha256=z9oxdKme1hMPuvk7mBiKR_tuVY8TqH77aTYqMgvEGl8,876
1706
1707
  fastlife/domain/model/types.py,sha256=64jJKFAi5x0e3vr8naHU1m_as0Qy8MS-s9CG0z6K1qc,381
@@ -1720,7 +1721,7 @@ fastlife/service/registry.py,sha256=0r8dVCF44JUugRctL9sDQjnHDV7SepH06OfkV6KE-4s,
1720
1721
  fastlife/service/request_factory.py,sha256=9o4B_78qrKPXQAq8A_RDhzAqCHdt6arV96Bq_JByyIM,931
1721
1722
  fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
1722
1723
  fastlife/service/templates.py,sha256=xNMKH-jNkEoCscO04H-QlzTqg-0pYbF_fc65xG-2rzs,2575
1723
- fastlife/service/translations.py,sha256=cAfvUlLM3KcgQjlD9PtEpZpTMctXKM_CUAmUeKw9n4M,6901
1724
+ fastlife/service/translations.py,sha256=UrkITvfdfw68GzWn_uFlCjjhRvwhc0aCJPnEr_Y1rK8,7151
1724
1725
  fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
1725
1726
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
1726
1727
  fastlife/shared_utils/infer.py,sha256=0GflLkaWJ-4LZ1Ig3moR-_o55wwJ_p_vJ4xo-yi3lyA,1406
@@ -1730,11 +1731,11 @@ fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,
1730
1731
  fastlife/testing/dom.py,sha256=q2GFrHWjwKMMTR0dsP3J-rXSxojZy8rOQ-07h2gfLKA,5869
1731
1732
  fastlife/testing/form.py,sha256=diiGfVMfNt19JTNUxlnbGfcbskR3ZMpk0Y-A57vfShc,7871
1732
1733
  fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
1733
- fastlife/testing/testclient.py,sha256=4LLw_QchEzdcdIobtIEzCABNebzyzVPEMj1tjdXQU_Y,6984
1734
+ fastlife/testing/testclient.py,sha256=gqgHQalhrLLZ8eveN2HeuoG9ne8CwxCm-Ll4b7jo9Xo,7249
1734
1735
  fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
1735
- fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
1736
- fastlifeweb-0.26.1.dist-info/METADATA,sha256=ZrtYyrVue0a2wbR9S0wALG-3COPtbvSZVKHitRfu3t4,3690
1737
- fastlifeweb-0.26.1.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
1738
- fastlifeweb-0.26.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1739
- fastlifeweb-0.26.1.dist-info/licenses/LICENSE,sha256=JFWuiKYRXKKMEAsX0aZp3hBcju-HYflJ2rwJAGwbCJo,1080
1740
- fastlifeweb-0.26.1.dist-info/RECORD,,
1736
+ fastlife/views/pydantic_form.py,sha256=M4uGP-QiDuSyrkYAsvSVJYZzdBUPOmCghQdwtR28K5E,1630
1737
+ fastlifeweb-0.27.0.dist-info/METADATA,sha256=5cvmsfd1YF7lhRZtH53Ip9Qxd7EwE54AQK4pFV2IChI,3690
1738
+ fastlifeweb-0.27.0.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
1739
+ fastlifeweb-0.27.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1740
+ fastlifeweb-0.27.0.dist-info/licenses/LICENSE,sha256=JFWuiKYRXKKMEAsX0aZp3hBcju-HYflJ2rwJAGwbCJo,1080
1741
+ fastlifeweb-0.27.0.dist-info/RECORD,,