fastlifeweb 0.26.2__py3-none-any.whl → 0.27.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +6 -0
- fastlife/adapters/jinjax/widgets/base.py +1 -1
- fastlife/adapters/jinjax/widgets/boolean.py +1 -1
- fastlife/adapters/jinjax/widgets/checklist.py +2 -2
- fastlife/adapters/jinjax/widgets/dropdown.py +2 -2
- fastlife/adapters/jinjax/widgets/mfa_code.py +1 -1
- fastlife/adapters/jinjax/widgets/model.py +1 -1
- fastlife/adapters/jinjax/widgets/sequence.py +1 -1
- fastlife/adapters/jinjax/widgets/text.py +12 -6
- fastlife/adapters/jinjax/widgets/union.py +2 -2
- fastlife/assets/dist.css +4 -1
- fastlife/components/A.jinja +5 -1
- fastlife/components/Input.jinja +2 -2
- fastlife/components/Label.jinja +1 -1
- fastlife/config/configurator.py +3 -10
- fastlife/config/resources.py +9 -3
- fastlife/domain/model/request.py +10 -0
- fastlife/service/translations.py +34 -22
- fastlife/template_globals.py +3 -0
- fastlife/views/pydantic_form.py +5 -0
- {fastlifeweb-0.26.2.dist-info → fastlifeweb-0.27.1.dist-info}/METADATA +2 -2
- {fastlifeweb-0.26.2.dist-info → fastlifeweb-0.27.1.dist-info}/RECORD +25 -25
- {fastlifeweb-0.26.2.dist-info → fastlifeweb-0.27.1.dist-info}/WHEEL +1 -1
- {fastlifeweb-0.26.2.dist-info → fastlifeweb-0.27.1.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.26.2.dist-info → fastlifeweb-0.27.1.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## 0.27.1 - Released on 2025-07-10
|
2
|
+
* Add response_model to @resource_view in order to document views that use StreamResponse.
|
3
|
+
|
4
|
+
## 0.27.0 - Released on 2025-04-24
|
5
|
+
* Add i18n support on pydantic form.
|
6
|
+
|
1
7
|
## 0.26.2 - Released on 2025-04-20
|
2
8
|
* Add a RedirectResponse class that is htmx friendly and P/R/G pattern friendly.
|
3
9
|
* Fix documentation generation.
|
@@ -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
|
-
"
|
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
|
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>
|
fastlife/assets/dist.css
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
/*! tailwindcss v4.1.
|
1
|
+
/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */
|
2
2
|
@layer properties;
|
3
3
|
@layer theme, base, components, utilities;
|
4
4
|
@layer theme {
|
@@ -288,6 +288,9 @@
|
|
288
288
|
.cursor-pointer {
|
289
289
|
cursor: pointer;
|
290
290
|
}
|
291
|
+
.appearance-none {
|
292
|
+
appearance: none;
|
293
|
+
}
|
291
294
|
.items-center {
|
292
295
|
align-items: center;
|
293
296
|
}
|
fastlife/components/A.jinja
CHANGED
@@ -20,13 +20,17 @@
|
|
20
20
|
"specify how the response will be swapped in relative to the target of an AJAX request."
|
21
21
|
] = "innerHTML show:body:top",
|
22
22
|
hx_push_url: Annotated[bool, "replace the browser url with the link."] = True,
|
23
|
+
hx_get: Annotated[
|
24
|
+
str | None,
|
25
|
+
"Override the target link only for htmx request for component rendering. href will be used if None."
|
26
|
+
] = None,
|
23
27
|
disable_htmx: Annotated[bool, "do not add any `hx-*` attibute to the link."] = False
|
24
28
|
#}
|
25
29
|
|
26
30
|
<a href="{{href}}"
|
27
31
|
{%- if id %} id="{{ id }}" {%- endif %}
|
28
32
|
{%- if not disable_htmx %}
|
29
|
-
hx-get="{{ href }}"
|
33
|
+
hx-get="{{ hx_get or href }}"
|
30
34
|
hx-target="{{ hx_target }}"
|
31
35
|
hx-swap="{{ hx_swap }}"
|
32
36
|
{%- if hx_push_url %} hx-push-url="true" {%- endif %}
|
fastlife/components/Input.jinja
CHANGED
@@ -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}}"
|
fastlife/components/Label.jinja
CHANGED
fastlife/config/configurator.py
CHANGED
@@ -368,7 +368,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
368
368
|
deprecated: bool | None = None,
|
369
369
|
methods: list[str] | None = None,
|
370
370
|
operation_id: str | None = None,
|
371
|
-
|
371
|
+
response_model: Any = None,
|
372
372
|
response_model_include: IncEx | None = None,
|
373
373
|
response_model_exclude: IncEx | None = None,
|
374
374
|
response_model_by_alias: bool = True,
|
@@ -433,7 +433,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
433
433
|
self._current_router.add_api_route(
|
434
434
|
path,
|
435
435
|
endpoint,
|
436
|
-
|
436
|
+
response_model=response_model,
|
437
437
|
status_code=status_code,
|
438
438
|
tags=tags,
|
439
439
|
dependencies=dependencies,
|
@@ -493,14 +493,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
493
493
|
custom_globals[key] = val
|
494
494
|
return {
|
495
495
|
"request": request,
|
496
|
-
|
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
|
}
|
fastlife/config/resources.py
CHANGED
@@ -99,6 +99,7 @@ def resource(
|
|
99
99
|
response_description=endpoint.response_description,
|
100
100
|
deprecated=endpoint.deprecated,
|
101
101
|
operation_id=endpoint.operation_id,
|
102
|
+
response_model=endpoint.response_model,
|
102
103
|
response_model_include=endpoint.response_model_include,
|
103
104
|
response_model_exclude=endpoint.response_model_exclude,
|
104
105
|
response_model_by_alias=endpoint.response_model_by_alias,
|
@@ -146,6 +147,7 @@ def resource_view(
|
|
146
147
|
deprecated: bool | None = None,
|
147
148
|
methods: list[str] | None = None,
|
148
149
|
operation_id: str | None = None,
|
150
|
+
response_model: type[Any] | None = None,
|
149
151
|
response_model_include: IncEx | None = None,
|
150
152
|
response_model_exclude: IncEx | None = None,
|
151
153
|
response_model_by_alias: bool = True,
|
@@ -155,7 +157,7 @@ def resource_view(
|
|
155
157
|
include_in_schema: bool = True,
|
156
158
|
openapi_extra: dict[str, Any] | None = None,
|
157
159
|
) -> Callable[..., Any]:
|
158
|
-
"""
|
160
|
+
"""
|
159
161
|
Decorator to use on a method of a class decorated with {func}`resource` in order
|
160
162
|
to add OpenAPI information.
|
161
163
|
|
@@ -177,8 +179,11 @@ def resource_view(
|
|
177
179
|
:param include_in_schema: Expose or not the route in the OpenAPI schema and
|
178
180
|
documentation.
|
179
181
|
|
180
|
-
:param
|
181
|
-
|
182
|
+
:param response_model: class used for the api documentation for streaming response.
|
183
|
+
It may also be used to validate data in case the view return dict.
|
184
|
+
See [FastAPI doc](https://fastapi.tiangolo.com/tutorial/response-model/).
|
185
|
+
:param response_model_include: customize fields list to include in response.
|
186
|
+
:param response_model_exclude: customize fields list to exclude in response.
|
182
187
|
:param response_model_by_alias: serialize fields by alias or by name if False.
|
183
188
|
:param response_model_exclude_unset: exclude fields that are not explicitly
|
184
189
|
set in response.
|
@@ -197,6 +202,7 @@ def resource_view(
|
|
197
202
|
fn.deprecated = deprecated
|
198
203
|
fn.methods = methods
|
199
204
|
fn.operation_id = operation_id
|
205
|
+
fn.response_model = response_model
|
200
206
|
fn.response_model_include = response_model_include
|
201
207
|
fn.response_model_exclude = response_model_exclude
|
202
208
|
fn.response_model_by_alias = response_model_by_alias
|
fastlife/domain/model/request.py
CHANGED
@@ -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)
|
fastlife/service/translations.py
CHANGED
@@ -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
|
104
|
-
return
|
105
|
-
|
106
|
-
|
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, **
|
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
|
-
|
146
|
+
/,
|
147
|
+
**mapping: Any,
|
137
148
|
) -> str:
|
138
149
|
ret = self.translations[domain].ngettext(singular, plural, n)
|
139
|
-
mapping_num = {"num": n, **
|
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
|
-
|
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
|
-
|
178
|
+
/,
|
179
|
+
**mapping: Any,
|
169
180
|
) -> str:
|
170
181
|
ret = self.global_translations.npgettext(context, singular, plural, n)
|
171
|
-
mapping_num = {"num": n, **
|
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
|
-
|
192
|
+
/,
|
193
|
+
**mapping: Any,
|
182
194
|
) -> str:
|
183
195
|
ret = self.translations[domain].npgettext(context, singular, plural, n)
|
184
|
-
mapping_num = {"num": n, **
|
196
|
+
mapping_num = {"num": n, **mapping}
|
185
197
|
return ret.format(**mapping_num)
|
186
198
|
|
187
199
|
|
fastlife/template_globals.py
CHANGED
@@ -29,6 +29,7 @@ class Globals(BaseModel):
|
|
29
29
|
"""Default css class for {jinjax:component}`A`."""
|
30
30
|
|
31
31
|
BUTTON_CLASS: str = space_join(
|
32
|
+
"appearance-none",
|
32
33
|
"bg-primary-600",
|
33
34
|
"px-5",
|
34
35
|
"py-2.5",
|
@@ -52,6 +53,7 @@ class Globals(BaseModel):
|
|
52
53
|
"""Default css class for {jinjax:component}`Details`."""
|
53
54
|
|
54
55
|
SECONDARY_BUTTON_CLASS: str = space_join(
|
56
|
+
"appearance-none",
|
55
57
|
"bg-neutral-300",
|
56
58
|
"px-5",
|
57
59
|
"py-2.5",
|
@@ -79,6 +81,7 @@ class Globals(BaseModel):
|
|
79
81
|
"""
|
80
82
|
|
81
83
|
ICON_BUTTON_CLASS: str = space_join(
|
84
|
+
"appearance-none",
|
82
85
|
"bg-white",
|
83
86
|
"p-1",
|
84
87
|
"rounded-xs",
|
fastlife/views/pydantic_form.py
CHANGED
@@ -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.
|
3
|
+
Version: 0.27.1
|
4
4
|
Summary: High-level web framework
|
5
5
|
Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
|
6
6
|
License: MIT
|
@@ -77,7 +77,7 @@ to maintain and scale your project.
|
|
77
77
|
|
78
78
|
## Tests
|
79
79
|
|
80
|
-
Fastlife
|
80
|
+
Fastlife comes with [a test client](https://mardiros.github.io/fastlife/develop/fastlife/fastlife.testing.testclient.html) that can interact with html inside unit tests.
|
81
81
|
|
82
82
|
|
83
83
|
## Try it
|
@@ -1,4 +1,4 @@
|
|
1
|
-
CHANGELOG.md,sha256=
|
1
|
+
CHANGELOG.md,sha256=EdwLpgFV0D5skxaZ0DbAtJPkk2QwLDt8sGNVqSGQJ28,9032
|
2
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
|
@@ -32,19 +32,19 @@ 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=
|
36
|
-
fastlife/adapters/jinjax/widgets/boolean.py,sha256=
|
37
|
-
fastlife/adapters/jinjax/widgets/checklist.py,sha256=
|
38
|
-
fastlife/adapters/jinjax/widgets/dropdown.py,sha256=
|
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=
|
41
|
-
fastlife/adapters/jinjax/widgets/model.py,sha256=
|
42
|
-
fastlife/adapters/jinjax/widgets/sequence.py,sha256=
|
43
|
-
fastlife/adapters/jinjax/widgets/text.py,sha256=
|
44
|
-
fastlife/adapters/jinjax/widgets/union.py,sha256=
|
45
|
-
fastlife/assets/dist.css,sha256=
|
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
|
+
fastlife/assets/dist.css,sha256=xuD2jjSO5GMCjyr_rmhcazJQ5wQpcYUvFxKm0RcC1XY,19801
|
46
46
|
fastlife/assets/source.css,sha256=0KtDcsKHj9LOcqNR1iv9pACwNBaNWkieEDqqjkgNL_s,47
|
47
|
-
fastlife/components/A.jinja,sha256=
|
47
|
+
fastlife/components/A.jinja,sha256=hTZeYWXKLgnqPucSrF2IY7TDP4d_xNfU2Rjl-bXnpGc,1548
|
48
48
|
fastlife/components/Button.jinja,sha256=itKU-ct45XissU33yfmTekyHsNe00fr4RQL-e9cxbgU,2305
|
49
49
|
fastlife/components/Checkbox.jinja,sha256=g62A1LR8TaN60h94pE2e5l9_eMmgnhVVE9HVCQtVVMo,748
|
50
50
|
fastlife/components/CsrfToken.jinja,sha256=mS0q-3_hAevl_waWEPaN0QAYOBzMyzl-W1PSpEHUBA0,215
|
@@ -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=
|
61
|
-
fastlife/components/Label.jinja,sha256=
|
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,17 +1690,17 @@ 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=
|
1693
|
+
fastlife/config/configurator.py,sha256=WVV6UIqVBQDNh_YuRIpRTo6O_-_Ut-ZqW8SNeMAujYI,24885
|
1694
1694
|
fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
|
1695
1695
|
fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
|
1696
|
-
fastlife/config/resources.py,sha256=
|
1696
|
+
fastlife/config/resources.py,sha256=stKCuZQGgiDW9xTrTNxKMb_JzkgkM1Ubum1b7OK4Lm4,8780
|
1697
1697
|
fastlife/config/views.py,sha256=9CZ0qNi8vKvQuGo1GgM6cwNK8WwHOxwIHqtikAOaOHY,2399
|
1698
1698
|
fastlife/domain/__init__.py,sha256=3zDDos5InVX0el9OO0lgSDGzdUNYIhlA6w4uhBh2pF8,29
|
1699
1699
|
fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wNuo2Y,27
|
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=
|
1703
|
+
fastlife/domain/model/request.py,sha256=hHtGsfVND3TSG7HQZI_I0n4Gp4KyaAtsjaZEc4lwrv0,3090
|
1704
1704
|
fastlife/domain/model/response.py,sha256=Vsd2zYGGhH0D2DlfiKz1CX9OJZ_ZYoEv_-foMZpDFZo,1294
|
1705
1705
|
fastlife/domain/model/security_policy.py,sha256=f9SLi54vvRU-KSPJ5K0unoqYpkxIyzuZjKf2Ylwf5Rg,4796
|
1706
1706
|
fastlife/domain/model/template.py,sha256=z9oxdKme1hMPuvk7mBiKR_tuVY8TqH77aTYqMgvEGl8,876
|
@@ -1721,21 +1721,21 @@ fastlife/service/registry.py,sha256=0r8dVCF44JUugRctL9sDQjnHDV7SepH06OfkV6KE-4s,
|
|
1721
1721
|
fastlife/service/request_factory.py,sha256=9o4B_78qrKPXQAq8A_RDhzAqCHdt6arV96Bq_JByyIM,931
|
1722
1722
|
fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
|
1723
1723
|
fastlife/service/templates.py,sha256=xNMKH-jNkEoCscO04H-QlzTqg-0pYbF_fc65xG-2rzs,2575
|
1724
|
-
fastlife/service/translations.py,sha256=
|
1724
|
+
fastlife/service/translations.py,sha256=UrkITvfdfw68GzWn_uFlCjjhRvwhc0aCJPnEr_Y1rK8,7151
|
1725
1725
|
fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
|
1726
1726
|
fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
|
1727
1727
|
fastlife/shared_utils/infer.py,sha256=0GflLkaWJ-4LZ1Ig3moR-_o55wwJ_p_vJ4xo-yi3lyA,1406
|
1728
1728
|
fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB39E8,3713
|
1729
|
-
fastlife/template_globals.py,sha256=
|
1729
|
+
fastlife/template_globals.py,sha256=pn2fWRE5QwYii4K0XJtfCt1hUmN8VvXXwAOWSwRFe94,9102
|
1730
1730
|
fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
|
1731
1731
|
fastlife/testing/dom.py,sha256=q2GFrHWjwKMMTR0dsP3J-rXSxojZy8rOQ-07h2gfLKA,5869
|
1732
1732
|
fastlife/testing/form.py,sha256=diiGfVMfNt19JTNUxlnbGfcbskR3ZMpk0Y-A57vfShc,7871
|
1733
1733
|
fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
|
1734
1734
|
fastlife/testing/testclient.py,sha256=gqgHQalhrLLZ8eveN2HeuoG9ne8CwxCm-Ll4b7jo9Xo,7249
|
1735
1735
|
fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
|
1736
|
-
fastlife/views/pydantic_form.py,sha256=
|
1737
|
-
fastlifeweb-0.
|
1738
|
-
fastlifeweb-0.
|
1739
|
-
fastlifeweb-0.
|
1740
|
-
fastlifeweb-0.
|
1741
|
-
fastlifeweb-0.
|
1736
|
+
fastlife/views/pydantic_form.py,sha256=M4uGP-QiDuSyrkYAsvSVJYZzdBUPOmCghQdwtR28K5E,1630
|
1737
|
+
fastlifeweb-0.27.1.dist-info/METADATA,sha256=Ztrqc7ngoilivkBACoQ2P94LAbPHQhuEORjLkiO7weQ,3691
|
1738
|
+
fastlifeweb-0.27.1.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
1739
|
+
fastlifeweb-0.27.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
1740
|
+
fastlifeweb-0.27.1.dist-info/licenses/LICENSE,sha256=JFWuiKYRXKKMEAsX0aZp3hBcju-HYflJ2rwJAGwbCJo,1080
|
1741
|
+
fastlifeweb-0.27.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|